diff --git a/.github/.nvmrc b/.github/.nvmrc index 248216ad5b..3fe3b1570a 100644 --- a/.github/.nvmrc +++ b/.github/.nvmrc @@ -1 +1 @@ -24.12.0 +24.13.0 diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 239a448bf6..b8ce6387af 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -178,9 +178,12 @@ 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 with: @@ -266,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 diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index db7ca0f57b..3591539b68 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -24,10 +24,11 @@ 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 @@ -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 diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 1933b9d572..8c0bf76f30 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -131,7 +131,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@b868e6e7c8cc212beec876330b4059e661ee44bb # use-mise-action-v1.1.1 + uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0 - name: Load parameters id: parameters diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml index 80cc17d32b..a7d068cb43 100644 --- a/.github/workflows/docs-destroy.yml +++ b/.github/workflows/docs-destroy.yml @@ -29,7 +29,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup Mise - uses: immich-app/devtools/actions/use-mise@b868e6e7c8cc212beec876330b4059e661ee44bb # use-mise-action-v1.1.1 + uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0 - name: Destroy Docs Subdomain env: diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 1a4c2b7945..373fbaf6c1 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -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: diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 2446b5ffcd..bd2c292ad5 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -12,6 +12,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + id-token: write + packages: write defaults: run: working-directory: ./open-api/typescript-sdk @@ -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/test.yml b/.github/workflows/test.yml index 2aed8c6da2..28a74ff33f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -502,14 +502,25 @@ jobs: - name: Run e2e tests (web) env: CI: true - run: npx playwright test + run: npx playwright test --project=chromium if: ${{ !cancelled() }} - - name: Archive test results + - name: Archive 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: + CI: true + run: npx playwright test --project=ui + if: ${{ !cancelled() }} + - name: Archive ui results + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + if: success() || failure() + with: + name: e2e-ui-test-results-${{ matrix.runner }} + path: e2e/playwright-report/ success-check-e2e: name: End-to-End Tests Success needs: [e2e-tests-server-cli, e2e-tests-web] @@ -580,9 +591,9 @@ jobs: - 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/ diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index e37497b9bb..cb11a11be4 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -36,7 +36,7 @@ jobs: github-token: ${{ steps.token.outputs.token }} filters: | i18n: - - modified: 'i18n/!(en)**\.json' + - modified: 'i18n/!(en|package)**\.json' skip-force-logic: 'true' enforce-lock: diff --git a/cli/.nvmrc b/cli/.nvmrc index 248216ad5b..3fe3b1570a 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -24.12.0 +24.13.0 diff --git a/cli/package.json b/cli/package.json index 99c13db08a..23a2ec062d 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.105", + "version": "2.5.2", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", @@ -20,7 +20,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^24.10.4", + "@types/node": "^24.10.9", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -69,6 +69,6 @@ "micromatch": "^4.0.8" }, "volta": { - "node": "24.12.0" + "node": "24.13.0" } } 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.prod.yml b/docker/docker-compose.prod.yml index 11417a4204..e250f5065b 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -85,7 +85,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:2b6f734e372c1b4717008f7d0a0152316aedd4d13ae17ef1e3268dbfaf68041b + image: prom/prometheus@sha256:1f0f50f06acaceb0f5670d2c8a658a599affe7b0d8e78b898c1035653849a702 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus diff --git a/docs/.nvmrc b/docs/.nvmrc index 248216ad5b..3fe3b1570a 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -24.12.0 +24.13.0 diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index 2ca965624f..111a39ac8d 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,47 +13,127 @@ 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. @@ -106,10 +188,12 @@ 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. +:::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. ::: ## Filesystem @@ -157,17 +241,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 +284,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/postgres-standalone.md b/docs/docs/administration/postgres-standalone.md index 4fc354aad7..84681fdfa6 100644 --- a/docs/docs/administration/postgres-standalone.md +++ b/docs/docs/administration/postgres-standalone.md @@ -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/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/setup.md b/docs/docs/developer/setup.md index fbda3c2983..8262d6a0d0 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -37,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. 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..4b477600f4 100644 --- a/docs/docs/features/command-line-interface.md +++ b/docs/docs/features/command-line-interface.md @@ -188,6 +188,8 @@ immich upload --dry-run . | 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/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..2fb5a1c56a 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -118,46 +118,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/mobile-app.mdx b/docs/docs/features/mobile-app.mdx index 8b9a204741..02b5d492f4 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. + +:::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/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/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/environment-variables.md b/docs/docs/install/environment-variables.md index a7494d5415..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. 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/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..147f981aff 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -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', diff --git a/docs/package.json b/docs/package.json index 4076d089ce..87b0b3fccd 100644 --- a/docs/package.json +++ b/docs/package.json @@ -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.12.0" + "node": "24.13.0" } } 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/static/_redirects b/docs/static/_redirects index ce4b670246..5d4ad14f00 100644 --- a/docs/static/_redirects +++ b/docs/static/_redirects @@ -34,3 +34,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 a3fd0be914..d2a25bf4b6 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,36 +1,20 @@ [ + { + "label": "v2.5.2", + "url": "https://docs.v2.5.2.archive.immich.app" + }, { "label": "v2.4.1", "url": "https://docs.v2.4.1.archive.immich.app" }, - { - "label": "v2.4.0", - "url": "https://docs.v2.4.0.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" @@ -39,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/.nvmrc b/e2e/.nvmrc index 248216ad5b..3fe3b1570a 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -24.12.0 +24.13.0 diff --git a/e2e/package.json b/e2e/package.json index c42bf6eddb..b3973eb8bf 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "2.4.1", + "version": "2.5.2", "description": "", "main": "index.js", "type": "module", @@ -27,7 +27,7 @@ "@playwright/test": "^1.44.1", "@socket.io/component-emitter": "^3.1.2", "@types/luxon": "^3.4.2", - "@types/node": "^24.10.4", + "@types/node": "^24.10.9", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", "@types/supertest": "^6.0.2", @@ -52,6 +52,6 @@ "vitest": "^3.0.0" }, "volta": { - "node": "24.12.0" + "node": "24.13.0" } } diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 4ae542bacf..58f5997343 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -8,7 +8,7 @@ dotenv.config({ 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'; @@ -40,9 +40,9 @@ const config: PlaywrightTestConfig = { workers: 1, }, { - name: 'parallel tests', + name: 'ui', use: { ...devices['Desktop Chrome'] }, - testMatch: /.*\.parallel-e2e-spec\.ts/, + testMatch: /.*\.ui-spec\.ts/, fullyParallel: true, workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1), }, diff --git a/e2e/src/api/specs/database-backups.e2e-spec.ts b/e2e/src/api/specs/database-backups.e2e-spec.ts new file mode 100644 index 0000000000..2b0f6ae61a --- /dev/null +++ b/e2e/src/api/specs/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/api/specs/maintenance.e2e-spec.ts index b6c7540bc5..8e4e154328 100644 --- a/e2e/src/api/specs/maintenance.e2e-spec.ts +++ b/e2e/src/api/specs/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/generators/memory.ts b/e2e/src/generators/memory.ts new file mode 100644 index 0000000000..c17b4aa476 --- /dev/null +++ b/e2e/src/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/generators/memory/model-objects.ts b/e2e/src/generators/memory/model-objects.ts new file mode 100644 index 0000000000..1bcc703ed8 --- /dev/null +++ b/e2e/src/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/generators/timeline/rest-response'; +import type { MockTimelineAsset } from 'src/generators/timeline/timeline-config'; +import { SeededRandom, selectRandomMultiple } from 'src/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/rest-response.ts b/e2e/src/generators/timeline/rest-response.ts index 6fcfe52fc2..a193535cd3 100644 --- a/e2e/src/generators/timeline/rest-response.ts +++ b/e2e/src/generators/timeline/rest-response.ts @@ -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/mock-network/memory-network.ts b/e2e/src/mock-network/memory-network.ts new file mode 100644 index 0000000000..9a3a9e6555 --- /dev/null +++ b/e2e/src/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/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/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts b/e2e/src/web/specs/asset-viewer/asset-viewer.ui-spec.ts similarity index 56% rename from e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts rename to e2e/src/web/specs/asset-viewer/asset-viewer.ui-spec.ts index eaf9d0d073..669f1b815c 100644 --- a/e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts +++ b/e2e/src/web/specs/asset-viewer/asset-viewer.ui-spec.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { Changes, createDefaultTimelineConfig, @@ -12,7 +12,7 @@ import { import { setupBaseMockApiRoutes } from 'src/mock-network/base-network'; import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network'; import { utils } from 'src/utils'; -import { assetViewerUtils, cancelAllPollers } from 'src/web/specs/timeline/utils'; +import { assetViewerUtils } from 'src/web/specs/timeline/utils'; test.describe.configure({ mode: 'parallel' }); test.describe('asset-viewer', () => { @@ -49,7 +49,6 @@ test.describe('asset-viewer', () => { }); test.afterEach(() => { - cancelAllPollers(); testContext.slowBucket = false; changes.albumAdditions = []; changes.assetDeletions = []; @@ -58,6 +57,120 @@ test.describe('asset-viewer', () => { }); 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}`); diff --git a/e2e/src/web/specs/database-backups.e2e-spec.ts b/e2e/src/web/specs/database-backups.e2e-spec.ts new file mode 100644 index 0000000000..d101215ceb --- /dev/null +++ b/e2e/src/web/specs/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/web/specs/maintenance.e2e-spec.ts index 534c05f783..8b1631f0bf 100644 --- a/e2e/src/web/specs/maintenance.e2e-spec.ts +++ b/e2e/src/web/specs/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/web/specs/memory/memory-viewer.ui-spec.ts b/e2e/src/web/specs/memory/memory-viewer.ui-spec.ts new file mode 100644 index 0000000000..11e73fbe25 --- /dev/null +++ b/e2e/src/web/specs/memory/memory-viewer.ui-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/generators/memory'; +import { + Changes, + createDefaultTimelineConfig, + generateTimelineData, + TimelineAssetConfig, + TimelineData, +} from 'src/generators/timeline'; +import { setupBaseMockApiRoutes } from 'src/mock-network/base-network'; +import { MemoryChanges, setupMemoryMockApiRoutes } from 'src/mock-network/memory-network'; +import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network'; +import { memoryAssetViewerUtils, memoryGalleryUtils, memoryViewerUtils } from 'src/web/specs/memory/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/web/specs/memory/utils.ts b/e2e/src/web/specs/memory/utils.ts new file mode 100644 index 0000000000..cf99033e7e --- /dev/null +++ b/e2e/src/web/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/web/specs/photo-viewer.e2e-spec.ts b/e2e/src/web/specs/photo-viewer.e2e-spec.ts index c8a9b42b2a..3f9bb4237a 100644 --- a/e2e/src/web/specs/photo-viewer.e2e-spec.ts +++ b/e2e/src/web/specs/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/search/search-gallery.ui-spec.ts b/e2e/src/web/specs/search/search-gallery.ui-spec.ts new file mode 100644 index 0000000000..e358bed154 --- /dev/null +++ b/e2e/src/web/specs/search/search-gallery.ui-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/generators/timeline'; +import { setupBaseMockApiRoutes } from 'src/mock-network/base-network'; +import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network'; +import { assetViewerUtils } from 'src/web/specs/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/web/specs/timeline/timeline.ui-spec.ts similarity index 99% rename from e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts rename to e2e/src/web/specs/timeline/timeline.ui-spec.ts index 5faf8380d1..47026e2cd4 100644 --- a/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts +++ b/e2e/src/web/specs/timeline/timeline.ui-spec.ts @@ -18,7 +18,6 @@ import { pageRoutePromise, setupTimelineMockApiRoutes, TimelineTestContext } fro import { utils } from 'src/utils'; import { assetViewerUtils, - cancelAllPollers, padYearMonth, pageUtils, poll, @@ -64,7 +63,6 @@ test.describe('Timeline', () => { }); test.afterEach(() => { - cancelAllPollers(); testContext.slowBucket = false; changes.albumAdditions = []; changes.assetDeletions = []; diff --git a/e2e/src/web/specs/timeline/utils.ts b/e2e/src/web/specs/timeline/utils.ts index 0b49f02941..0f04bf9361 100644 --- a/e2e/src/web/specs/timeline/utils.ts +++ b/e2e/src/web/specs/timeline/utils.ts @@ -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; } @@ -181,8 +167,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/web/specs/user-admin.e2e-spec.ts b/e2e/src/web/specs/user-admin.e2e-spec.ts index 7a2cd77177..67a537ba9d 100644 --- a/e2e/src/web/specs/user-admin.e2e-spec.ts +++ b/e2e/src/web/specs/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/i18n/ar.json b/i18n/ar.json index 9ec02a31e3..968f9e02e7 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": "إدارة إعدادات السجلات", @@ -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": "يحتوي الأصل على وجوه غير مخصصة", @@ -646,6 +684,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 +743,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 +753,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": "مسح جميع عمليات البحث الأخيرة", @@ -779,6 +833,7 @@ "create_album": "إنشاء ألبوم", "create_album_page_untitled": "بدون اسم", "create_api_key": "إنشاء مفتاح API", + "create_first_workflow": "إنشاء سير العمل الأول", "create_library": "إنشاء مكتبة", "create_link": "إنشاء رابط", "create_link_to_share": "إنشاء رابط للمشاركة", @@ -793,17 +848,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 +922,7 @@ "deselect_all": "الغاء تحديد الكل", "details": "تفاصيل", "direction": "الإتجاه", + "disable": "ابطال", "disabled": "معطل", "disallow_edits": "منع التعديلات", "discord": "دسكورد", @@ -884,16 +948,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 +987,17 @@ "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_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 +1006,7 @@ "enable": "تفعيل", "enable_backup": "تشغيل النسخ الاحتياطي", "enable_biometric_auth_description": "أدخل رمز PIN الخاص بك لتمكين المصادقة البيومترية", - "enabled": "مفعل", + "enabled": "مفعَل", "end_date": "تاريخ الإنتهاء", "enqueued": "مُدرج في الطابور", "enter_wifi_name": "ادخل اسم Wi-Fi", @@ -944,11 +1016,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 +1081,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 +1092,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 +1132,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 +1144,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 +1179,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 +1192,16 @@ "features": "الميزات", "features_in_development": "الميزات قيد التطوير", "features_setting_description": "إدارة ميزات التطبيق", - "file_name": "إسم الملف", + "file_name": "اسم الملف: {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": "إصلاح المطابقة غير الصحيحة", @@ -1128,12 +1211,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 +1246,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 +1326,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 +1347,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 +1379,7 @@ "local": "محلّي", "local_asset_cast_failed": "غير قادر على بث أصل لم يتم تحميله إلى الخادم", "local_assets": "أُصول (ملفات) محلية", + "local_id": "الهوية المحلية", "local_media_summary": "ملخص الملفات المحلية", "local_network": "شبكة محلية", "local_network_sheet_info": "سيتصل التطبيق بالخادم من خلال عنوان URL هذا عند استخدام شبكة Wi-Fi المحددة", @@ -1332,10 +1431,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 +1514,8 @@ "minimize": "تصغير", "minute": "دقيقة", "minutes": "دقائق", + "mirror_horizontal": "افقي", + "mirror_vertical": "عمودي", "missing": "المفقودة", "mobile_app": "تطبيق الجوال", "mobile_app_download_onboarding_note": "قم بتنزيل التطبيق المصاحب للهاتف المحمول باستخدام الخيارات التالية", @@ -1405,11 +1524,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 +1541,7 @@ "my_albums": "ألبوماتي", "name": "الاسم", "name_or_nickname": "الاسم أو اللقب", + "name_required": "الاسم مطلوب", "navigate": "التنقل", "navigate_to_time": "انتقل إلى الوقت", "network_requirement_photos_upload": "استخدام بيانات الهاتف المحمول لعمل نسخة احتياطية للصور", @@ -1443,6 +1566,8 @@ "next": "التالي", "next_memory": "الذكرى التالية", "no": "لا", + "no_actions_added": "لم تتم إضافة إجراءات حتى الان", + "no_albums_found": "لم يتم ايجاد البومات", "no_albums_message": "قم بإنشاء ألبوم لتنظيم الصور ومقاطع الفيديو الخاصة بك", "no_albums_with_name_yet": "يبدو أنه ليس لديك أي ألبومات بهذا الاسم حتى الآن.", "no_albums_yet": "يبدو أنه ليس لديك أي ألبومات حتى الآن.", @@ -1452,11 +1577,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,6 +1597,7 @@ "no_results_description": "جرب كلمة رئيسية مرادفة أو أكثر عمومية", "no_shared_albums_message": "قم بإنشاء ألبوم لمشاركة الصور ومقاطع الفيديو مع الأشخاص في شبكتك", "no_uploads_in_progress": "لا يوجد اي ملفات قيد الرفع", + "none": "لا يوجد", "not_allowed": "غير مسموح", "not_available": "غير متاح", "not_in_any_album": "ليست في أي ألبوم", @@ -1552,6 +1680,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 +1705,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 +1788,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": "تم تعطيل وضع القراءة فقط", @@ -1759,9 +1893,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 +1927,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 +1962,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 +2114,7 @@ "show_password": "إظهار كلمة المرور", "show_person_options": "إظهار خيارات الشخص", "show_progress_bar": "إظهار شريط التقدم", + "show_schema": "أظهر المخطط", "show_search_options": "إظهار خيارات البحث", "show_shared_links": "عرض الروابط المشتركة", "show_slideshow_transition": "إظهار انتقال عرض الشرائح", @@ -1988,6 +2132,8 @@ "skip_to_folders": "تخطي إلى المجلدات", "skip_to_tags": "تخطي إلى العلامات", "slideshow": "عرض الشرائح", + "slideshow_repeat": "اعادة عرض الشرائح", + "slideshow_repeat_description": "العودة إلى البداية عند انتهاء عرض الشرائح", "slideshow_settings": "إعدادات عرض الشرائح", "sort_albums_by": "رتب الألبومات حسب...", "sort_created": "تاريخ الإنشاء", @@ -2064,6 +2210,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 +2245,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 +2266,7 @@ "unhide_person": "أظهر الشخص", "unknown": "غير معروف", "unknown_country": "بلد غير معروف", + "unknown_date": "تاريخ غير معروف", "unknown_year": "سنة غير معروفة", "unlimited": "غير محدود", "unlink_motion_video": "إلغاء ربط فيديو الحركة", @@ -2128,13 +2283,14 @@ "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": "هل تريد النسخ الاحتياطي للأصول (الأصول) المحددة إلى الخادم؟", @@ -2174,6 +2330,7 @@ "utilities": "أدوات", "validate": "تحقْق", "validate_endpoint_error": "الرجاء ادخال عنوان URL صالح", + "validation_error": "خطأ في التحقق", "variables": "المتغيرات", "version": "الإصدار", "version_announcement_closing": "صديقك، أليكس", @@ -2185,10 +2342,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 +2363,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..bd692531cc 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,15 @@ "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": "Назва файла", + "file_name": "Назва файла: {file_name}", "filename": "Назва файла", "filetype": "Тып файла", "filter": "Фільтр", @@ -427,6 +472,7 @@ "repository": "Рэпазіторый", "reset": "Скінуць", "reset_password": "Скінуць пароль", + "resolution": "Раздзяляльнасць", "restore": "Аднавіць", "restore_all": "Аднавіць усё", "restore_user": "Аднавіць карыстальніка", @@ -447,6 +493,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 +546,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..832e3e22fc 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": "Управление на настройките на записване", @@ -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": "Управление на шаблони за известия", @@ -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": "Елементът има незададени лица", @@ -691,7 +723,7 @@ "canceling": "Анулиране", "cannot_merge_people": "Не може да обединява хора", "cannot_undo_this_action": "Не можете да отмените това действие!", - "cannot_update_the_description": "Описанието не може да бъде актуализирано", + "cannot_update_the_description": "Описанието не може да бъде обновено", "cast": "Поточно предаване", "cast_description": "Настройка на наличните цели за предаване", "change_date": "Промени датата", @@ -711,6 +743,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 +756,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": "Изчистете всички скорошни търсения", @@ -787,6 +833,7 @@ "create_album": "Създай албум", "create_album_page_untitled": "Без заглавие", "create_api_key": "Създайте API ключ", + "create_first_workflow": "Създайте първи работен процес", "create_library": "Създай библиотека", "create_link": "Създай линк", "create_link_to_share": "Създаване на линк за споделяне", @@ -801,17 +848,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 +922,7 @@ "deselect_all": "Премахни избора от всички", "details": "Детайли", "direction": "Посока", + "disable": "Забрани", "disabled": "Изключено", "disallow_edits": "Забраняване на редакциите", "discord": "Намери ни в Discord", @@ -892,6 +948,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 +958,7 @@ "download_waiting_to_retry": "Изчакване за повторение", "downloading": "Изтегляне", "downloading_asset_filename": "Изтегляне на файл {filename}", + "downloading_from_icloud": "Сваляне от iCloud", "downloading_media": "Изтегляне на медия", "drop_files_to_upload": "Пуснете файловете, за да ги качите", "duplicates": "Дубликати", @@ -929,11 +987,17 @@ "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_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 +1016,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 +1081,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 +1092,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 +1132,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,10 +1142,12 @@ "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_bottom_sheet_description": "Добави Описание...", @@ -1116,18 +1188,20 @@ "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": "Име на файла: {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": "Поправяне на неправилно съвпадение", @@ -1137,12 +1211,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_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 +1253,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 +1326,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 +1431,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 +1514,8 @@ "minimize": "Минимизиране", "minute": "Минута", "minutes": "Минути", + "mirror_horizontal": "Хоризонтално", + "mirror_vertical": "Вертикално", "missing": "Липсващи", "mobile_app": "Мобилно приложение", "mobile_app_download_onboarding_note": "Свалете мобилното приложение Immich с някоя от следните опции", @@ -1416,11 +1524,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 +1541,7 @@ "my_albums": "Мои албуми", "name": "Име", "name_or_nickname": "Име или прякор", + "name_required": "Задължително е Име", "navigate": "Придвижване", "navigate_to_time": "Придвижване до момент във времето", "network_requirement_photos_upload": "Използвай мобилни данни за архивиране на снимки", @@ -1454,20 +1566,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,6 +1597,7 @@ "no_results_description": "Опитайте със синоним или по-обща ключова дума", "no_shared_albums_message": "Създайте албум, за да споделяте снимки и видеоклипове с хората в мрежата си", "no_uploads_in_progress": "Няма качване в момента", + "none": "Нищо", "not_allowed": "Не е разрешено", "not_available": "Неналично", "not_in_any_album": "Не е в никой албум", @@ -1563,6 +1680,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 +1705,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 +1788,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": "Режима само за четене е деактивиран", @@ -1770,9 +1893,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 +1927,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 +1962,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": "Изберете всичко за кошчето", @@ -1982,6 +2114,7 @@ "show_password": "Покажи паролата", "show_person_options": "Показване на опции за лица", "show_progress_bar": "Показване на прогрес бара", + "show_schema": "Покажи схема", "show_search_options": "Показване на опциите за търсене", "show_shared_links": "Покажи споделени линкове", "show_slideshow_transition": "Покажи прехода на слайдшоуто", @@ -1999,6 +2132,8 @@ "skip_to_folders": "Премини към папките", "skip_to_tags": "Премини към етикетите", "slideshow": "Слайдшоу", + "slideshow_repeat": "Повтаряй слайдшоуто", + "slideshow_repeat_description": "Започвай отново, когато слайдшоуто приключи", "slideshow_settings": "Настройки за слайдшоу", "sort_albums_by": "Сортиране на албуми по...", "sort_created": "Дата на създаване", @@ -2053,7 +2188,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 +2210,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 +2245,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 +2266,7 @@ "unhide_person": "Покажи отново човека", "unknown": "Неизвестно", "unknown_country": "Непозната Държава", + "unknown_date": "Неизвестна дата", "unknown_year": "Неизвестна година", "unlimited": "Неограничено", "unlink_motion_video": "Премахни връзката с видео", @@ -2139,13 +2283,14 @@ "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": "Искате ли да архивирате на сървъра избраните обекти?", @@ -2164,7 +2309,7 @@ "url": "URL", "usage": "Потребление", "use_biometric": "Използвай биометрия", - "use_current_connection": "използвай текущата връзка", + "use_current_connection": "Използвай текущата връзка", "use_custom_date_range": "Използвайте собствен диапазон от дати вместо това", "user": "Потребител", "user_has_been_deleted": "Този потребител е премахнат.", @@ -2185,6 +2330,7 @@ "utilities": "Инструменти", "validate": "Валидиране", "validate_endpoint_error": "Моля, въведи правилен URL", + "validation_error": "Грешка при валидиране", "variables": "Променливи", "version": "Версия", "version_announcement_closing": "Твой приятел, Алекс", @@ -2196,6 +2342,7 @@ "video_hover_setting_description": "Възпроизвеждане на видеоклипа, когато мишката се движи над елемента. Дори когато е деактивирано, възпроизвеждането може да бъде стартирано чрез задържане на курсора на мишката върху иконата за възпроизвеждане.", "videos": "Видеоклипове", "videos_count": "{count, plural, one {# Видео} other {# Видеа}}", + "videos_only": "Само видеа", "view": "Преглед", "view_album": "Разгледай албума", "view_all": "Преглед на всички", @@ -2216,6 +2363,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 +2373,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/bn.json b/i18n/bn.json index a785993f0a..0639e4e681 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,6 +38,7 @@ "add_to_album_toggle": "{album} - এর নির্বাচন পরিবর্তন করুন", "add_to_albums": "অ্যালবামে যোগ করুন", "add_to_albums_count": "অ্যালবামে যোগ করুন ({count})", + "add_to_bottom_bar": "এ যোগ করুন", "add_to_shared_album": "শেয়ার করা অ্যালবামে যোগ করুন", "add_url": "লিঙ্ক যোগ করুন", "added_to_archive": "আর্কাইভ এ যোগ করা হয়েছে", diff --git a/i18n/ca.json b/i18n/ca.json index 89fe1617cd..ad02cb3cfa 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", @@ -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", @@ -591,7 +623,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 +743,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 +756,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", @@ -787,6 +833,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 +848,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 +922,7 @@ "deselect_all": "Deseleccionar Tots", "details": "Detalls", "direction": "Direcció", + "disable": "Desactiva", "disabled": "Desactivat", "disallow_edits": "No permetre les edicions", "discord": "Discord", @@ -892,6 +948,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 +958,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 +987,17 @@ "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_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 +1016,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 +1081,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 +1092,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 +1132,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 +1144,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", @@ -1120,14 +1192,16 @@ "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": "Nom de l'arxiu: {file_name}", "file_name_or_extension": "Nom de l'arxiu o extensió", "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 +1211,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 +1253,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 +1326,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 +1431,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,6 +1514,8 @@ "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", @@ -1416,11 +1524,14 @@ "monthly_title_text_date_format": "MMMM y", "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 +1541,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 +1566,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,6 +1597,7 @@ "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", @@ -1563,6 +1680,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 +1705,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 +1788,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", @@ -1770,9 +1893,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 +1927,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 +1962,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 +2114,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 +2132,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ó", @@ -2075,6 +2210,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 +2245,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 +2266,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,13 +2283,14 @@ "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?", @@ -2164,7 +2309,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 +2330,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 +2342,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 +2363,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 +2373,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..3a916f1484 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", @@ -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í", @@ -787,6 +836,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 +851,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 +925,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 +951,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 +961,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 +990,17 @@ "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_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 +1019,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 +1084,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 +1095,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 +1135,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 +1147,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...", @@ -1120,14 +1195,16 @@ "features": "Funkce", "features_in_development": "Funkce ve vývoji", "features_setting_description": "Správa funkcí aplikace", - "file_name": "Název souboru", + "file_name": "Název souboru: {file_name}", "file_name_or_extension": "Název nebo přípona souboru", "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 +1214,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 +1256,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 +1329,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 +1434,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 +1517,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 +1527,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 +1544,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 +1569,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,6 +1600,7 @@ "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", @@ -1563,6 +1683,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 +1708,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 +1791,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", @@ -1770,9 +1896,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 +1930,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 +1965,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 +2117,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 +2135,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í", @@ -2075,6 +2213,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 +2248,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 +2269,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 +2286,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 +2313,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 +2334,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 +2346,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 +2367,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 +2377,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..d221e68907 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_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", @@ -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.", @@ -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", @@ -659,7 +691,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 +743,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 +756,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 sikkerhedskopierede filer fundet der matcher dine kriterier", + "cleanup_preview_title": "Filer at fjerne ({count})", + "cleanup_step3_description": "Scan efter fotos og videoer, der er blevet sikkerhedskopieret til serveren med den valgte stop-dato og filtermuligheder", + "cleanup_step4_summary": "{count} filer lavet før {date} er i kø for at blive fjernet fra denne enhed", + "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", @@ -787,6 +833,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 +848,24 @@ "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": "Fjern fotos og videoer ældre end", + "cutoff_day": "{antal, flertal, en {day} andre {days}}", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "dark": "Mørk", @@ -867,6 +921,7 @@ "deselect_all": "Afmarkér alt", "details": "DETALJER", "direction": "Retning", + "disable": "Deaktiver", "disabled": "Deaktiveret", "disallow_edits": "Deaktivér redigeringer", "discord": "Discord", @@ -892,6 +947,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 +957,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 +986,17 @@ "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_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,6 +1015,7 @@ "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_saving_image": "Fejl: {error}", @@ -1001,7 +1065,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 +1078,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 +1089,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 +1129,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 +1141,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 +1189,16 @@ "features": "Funktioner", "features_in_development": "Funktioner under udvikling", "features_setting_description": "Administrer app-funktioner", - "file_name": "Filnavn", + "file_name": "Filnavn: {file_name}", "file_name_or_extension": "Filnavn eller filtype", "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 +1208,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 skraldepapir 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 +1250,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 +1299,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,8 +1323,11 @@ "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_all": "Behold alle", + "keep_favorites": "Behold favoritter", "keep_this_delete_others": "Behold dette, slet andre", "kept_this_deleted_others": "Beholdt denne mediefil og slettede {count, plural, one {# aktiv} other {# aktiver}}", "keyboard_shortcuts": "Tastaturgenveje", @@ -1379,7 +1458,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 +1487,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 +1497,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 +1514,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,6 +1539,8 @@ "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.", @@ -1463,11 +1550,13 @@ "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 +1570,12 @@ "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", + "note_apply_storage_label_to_previously_uploaded assets": "Bemærk: For at anvende Lagringsmærkat på tidligere uploadede medier, kør opgaven igen", "notes": "Noter", "nothing_here_yet": "Intet her endnu", "notification_permission_dialog_content": "Gå til indstillinger for at slå notifikationer til.", @@ -1531,9 +1621,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 +1653,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 +1671,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_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 +1705,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 +1746,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,6 +1761,7 @@ "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}}", @@ -1685,8 +1780,8 @@ "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 +1833,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 +1865,11 @@ "saved_settings": "Gemte indstillinger", "say_something": "Skriv noget", "scaffold_body_error_occurred": "Der opstod en fejl", + "scan": "Scan", "scan_all_libraries": "Skan alle biblioteker", "scan_library": "Skan", "scan_settings": "Skanningsindstillinger", + "scanning": "Scanning", "scanning_for_album": "Skanner efter albummer...", "search": "Søg", "search_albums": "Søg i albummer", @@ -1802,6 +1899,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 +1934,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 +2006,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", @@ -1982,6 +2086,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 +2104,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", @@ -2109,12 +2216,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 +2237,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,13 +2254,14 @@ "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?", @@ -2164,7 +2280,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 +2297,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 +2313,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 +2334,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 +2344,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..b900e52513 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -1,10 +1,11 @@ { - "about": "Über Immich", + "about": "Über", "account": "Konto", "account_settings": "Kontoeinstellungen", "acknowledge": "Bestätigen", "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": "Speicher freigeben wird alle Fotos auf dem Gerät behalten", + "always_keep_videos_hint": "Speicher freigeben 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.", @@ -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", @@ -711,6 +743,8 @@ "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", @@ -722,6 +756,18 @@ "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", @@ -787,6 +833,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 +848,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 +922,7 @@ "deselect_all": "Alle abwählen", "details": "Details", "direction": "Richtung", + "disable": "Deaktivieren", "disabled": "Deaktiviert", "disallow_edits": "Bearbeitungen verbieten", "discord": "Discord", @@ -892,6 +948,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 +958,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 +987,17 @@ "edit_tag": "Tag bearbeiten", "edit_title": "Titel bearbeiten", "edit_user": "Nutzer bearbeiten", + "edit_workflow": "Workflow bearbeiten", "editor": "Bearbeiter", "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_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", @@ -952,11 +1016,14 @@ "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 +1081,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 +1092,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 +1132,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 +1144,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 +1192,16 @@ "features": "Funktionen", "features_in_development": "Feature in Entwicklung", "features_setting_description": "Funktionen der App verwalten", - "file_name": "Dateiname", + "file_name": "Dateiname: {file_name}", "file_name_or_extension": "Dateiname oder -erweiterung", "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,12 +1211,16 @@ "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_people_error": "Fehler beim Laden der Personen", "get_wifiname_error": "WLAN-Name konnte nicht ermittelt werden. Vergewissere dich, dass die erforderlichen Berechtigungen erteilt wurden und du mit einem WLAN-Netzwerk verbunden bist", "getting_started": "Erste Schritte", "go_back": "Zurück", @@ -1175,6 +1253,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 +1326,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", @@ -1343,10 +1431,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 +1514,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 +1524,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 +1541,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 +1566,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,6 +1597,7 @@ "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", @@ -1563,6 +1680,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 +1705,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 +1788,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 +1803,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,9 +1893,11 @@ "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", @@ -1802,6 +1927,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 +1954,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 +1962,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 +2114,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 +2132,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 +2142,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", @@ -2075,6 +2210,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 +2245,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 +2266,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,13 +2283,14 @@ "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?", @@ -2164,7 +2309,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 +2330,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 +2342,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 +2363,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", @@ -2224,13 +2373,26 @@ "welcome_to_immich": "Willkommen bei Immich", "width": "Breite", "wifi_name": "WLAN-Name", - "workflow": "Workflow", + "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", + "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..52bff4839f 100644 --- a/i18n/de_CH.json +++ b/i18n/de_CH.json @@ -1,38 +1,58 @@ { + "about": "Über", "account": "Konto", "account_settings": "Konto Istelligä", "acknowledge": "Bestätige", + "action": "Aktion", "action_common_update": "Update", + "action_description": "Eine Reihe von Aktionen, die an den gefilterten Assets ausgeführt werden sollen", + "actions": "Aktione", "active": "Aktiv", + "active_count": "Aktivi: {count}", "activity": "Aktivität", + "activity_changed": "Aktivität ist {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_birthday": "Geburtstag hinzuefüege", + "add_endpoint": "Endpunkt hinzuefüge", + "add_exclusion_pattern": "Exklusions muster hinzuefüege", + "add_filter": "Filter hinzuefüge", + "add_filter_description": "Klicken, um eine Filterbedingung hinzuzufügen", "add_location": "Standort hinzuefüege", "add_more_users": "Meh Benutzer hinzuefüege", + "add_partner": "Partner hinzufügen", "add_path": "Pfad hinzuefüege", "add_photos": "Föteli hinzuefüege", - "add_to": "Zu ... hinzuefüege", + "add_tag": "Tag hinzufügen", + "add_to": "Hinzuefüege zu …", "add_to_album": "Zum Album hinzuefüege", + "add_to_album_bottom_sheet_added": "Zu {album} hinzugefügt", + "add_to_album_bottom_sheet_already_exists": "Bereits in {album}", "add_to_album_bottom_sheet_some_local_assets": "Es hend es paar lokali Dateie nöd chöne im Album hinzuegfüegt werde", + "add_to_album_toggle": "Auswahl umschalten für {album}", "add_to_albums": "Zu Albe hinzuefüege", + "add_to_albums_count": "Zu Alben hinzufügen ({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 hinzufügen", "added_to_archive": "Is Archiv verschobe", "added_to_favorites": "Zu dine Favoritä hinzuegfüegt", + "added_to_favorites_count": "{count, number} zu Favoriten hinzugefügt", "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": "Ausschlussmuster hinzufügen. Platzhalter, wie *, **, und ? werden unterstützt. Um alle Dateien in einem Verzeichnis namens „Raw\" zu ignorieren, „**/Raw/**“ verwenden. Um alle Dateien zu ignorieren, die auf „.tif“ enden, „**/*.tif“ verwenden. Um einen absoluten Pfad zu ignorieren, „/pfad/zum/ignorieren/**“ verwenden.", "admin_user": "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": "Diese Datei einer externen Bibliothek befindet sich nicht mehr auf der Festplatte und wurde in den Papierkorb verschoben. Falls die Datei innerhalb der Bibliothek verschoben wurde, überprüfe deine Zeitleiste auf die neue entsprechende Datei. Um diese Datei wiederherzustellen, stelle bitte sicher, dass Immich auf den unten stehenden Dateipfad zugreifen kann und scanne die Bibliothek.", "authentication_settings": "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": "Nutze einen Server-Befehl zur Reaktivierung.", "background_task_job": "Hintergrund Ufgabä", "backup_database": "Datenbank-Dump aalege", "backup_database_enable_description": "Datenbank-Dumps aktiviere", @@ -51,6 +71,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": "Bist du sicher, dass du das Passwort für {user} zurücksetzen möchtest?", + "confirm_user_pin_code_reset": "Bist du sicher, dass du den PIN-Code von {user} zurücksetzen möchtest?", + "copy_config_to_clipboard_description": "Kopiere die aktuelle Systemkonfiguration als JSON-Objekt in die Zwischenablage", + "create_job": "Aufgabe erstellen", + "cron_expression": "Cron-Zeitangabe", + "cron_expression_description": "Setze das Scanintervall im Cron-Format. Hilfe mit dem Format bietet dir dabei z. B. der Crontab Guru", + "cron_expression_presets": "Vorlagen für Cron-Ausdruck", + "disable_login": "Login deaktiviere", + "duplicate_detection_job_description": "Diese Aufgabe führt das maschinelle Lernen für jede Datei aus, um Duplikate zu finden. Diese Aufgabe beruht auf der intelligenten Suche", + "exclusion_pattern_description": "Mit Ausschlussmustern können Dateien und Ordner beim Scannen Ihrer Bibliothek ignoriert werden. Dies ist nützlich, wenn du Ordner hast, die Dateien enthalten, die du nicht importieren möchtest, wie z. B. RAW-Dateien.", + "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..9df82ce4ad 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": "Για επανενεργοποίηση, χρησιμοποιήστε μία Εντολή Διακομιστή.", @@ -109,7 +116,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 +130,7 @@ "library_scanning": "Περιοδική Σάρωση", "library_scanning_description": "Ρύθμιση περιοδικής σάρωσης βιβλιοθήκης", "library_scanning_enable_description": "Ενεργοποίηση περιοδικής σάρωσης βιβλιοθήκης", - "library_settings": "Εξωτερική Βιβλιοθήκη", + "library_settings": "Εξωτερική βιβλιοθήκη", "library_settings_description": "Διαχείριση ρυθμίσεων εξωτερικής βιβλιοθήκης", "library_tasks_description": "Σάρωση εξωτερικών βιβλιοθηκών για νέα ή/και αλλαγμένα στοιχεία", "library_updated": "Ενημερωμένη βιβλιοθήκη", @@ -132,7 +139,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 +181,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": "Διαχείριση ρυθμίσεων αρχείου καταγραφής", @@ -294,7 +312,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 +334,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": "Ρυθμίσεις Συστήματος", @@ -332,7 +350,7 @@ "template_settings_description": "Διαχείριση προσαρμοσμένων προτύπων για ειδοποιήσεις", "theme_custom_css_settings": "Προσαρμοσμένο CSS", "theme_custom_css_settings_description": "Τα Cascading Style Sheets(CSS) επιτρέπει την προσαρμογή του σχεδιασμού του Immich.", - "theme_settings": "Ρυθμίσεις Θέματος", + "theme_settings": "Ρυθμίσεις θέματος", "theme_settings_description": "Διαχείριση της προσαρμογής του ιστότοπου του Immich", "thumbnail_generation_job": "Δημιουργία Μικρογραφιών", "thumbnail_generation_job_description": "Δημιουργία μεγάλων, μικρών και θολών μικρογραφιών για κάθε αρχείο, καθώς και μικρογραφιών για κάθε άτομο", @@ -399,7 +417,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 +440,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": "Μετατροπή βίντεο για μεγαλύτερη συμβατότητα με προγράμματα περιήγησης και συσκευές" @@ -467,10 +485,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,6 +508,7 @@ "albums_default_sort_order_description": "Αρχική ταξινόμηση κατά τη δημιουργία νέων άλμπουμ.", "albums_feature_description": "Συλλογές στοιχείων που μπορούν να κοινοποιηθούν σε άλλους χρήστες.", "albums_on_device_count": "Άλμπουμ στη συσκευή ({count})", + "albums_selected": "{count, plural, one {# άλμπουμ επιλέχθηκε} other {# άλμπουμ επιλέχθηκαν}}", "all": "Όλα", "all_albums": "Όλα τα άλμπουμ", "all_people": "Όλα τα άτομα", @@ -524,10 +545,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": "Το αντικείμενο έχει μη ανατεθειμένα πρόσωπα", @@ -591,7 +614,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 +734,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 +747,17 @@ "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_icloud_shared_albums_excluded": "Τα Κοινόχρηστα Άλμπουμ iCloud εξαιρούνται από τη σάρωση", + "cleanup_no_assets_found": "Δεν βρέθηκαν αντίγραφα ασφαλείας στοιχείων που να ταιριάζουν με τα κριτήρια σου", + "cleanup_preview_title": "Στοιχεία προς διαγραφή ({count})", + "cleanup_step3_description": "Σάρωση για φωτογραφίες και βίντεο που έχουν αντιγραφεί στον διακομιστή με την επιλεγμένη ημερομηνία και τα επιλεγμένα φίλτρα", + "cleanup_step4_summary": "{count} αρχεία που δημιουργήθηκαν πριν από {date} έχουν τοποθετηθεί σε σειρά για διαγραφή από τη συσκευή σας", + "cleanup_trash_hint": "Για την πλήρη απελευθέρωση του χώρου αποθήκευσης, ανοίξτε την εφαρμογή φωτογραφιών του συστήματός σας και αδειάστε τον κάδο", "clear": "Εκκαθάριση", "clear_all": "Εκκαθάριση όλων", "clear_all_recent_searches": "Εκκαθάριση όλων των πρόσφατων αναζητήσεων", @@ -787,6 +823,7 @@ "create_album": "Δημιουργία άλμπουμ", "create_album_page_untitled": "Χωρίς τίτλο", "create_api_key": "Δημιουργία κλειδιού API", + "create_first_workflow": "Δημιουργήστε την πρώτη ροή εργασίας", "create_library": "Δημιουργία Βιβλιοθήκης", "create_link": "Δημιουργία συνδέσμου", "create_link_to_share": "Δημιουργία συνδέσμου για διαμοιρασμό", @@ -801,17 +838,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 +912,7 @@ "deselect_all": "Ακύρωση όλων των επιλογών", "details": "Λεπτομέρειες", "direction": "Κατεύθυνση", + "disable": "Απενεργοποίηση", "disabled": "Απενεργοποιημένο", "disallow_edits": "Απαγόρευση επεξεργασιών", "discord": "Πλατφόρμα Discord", @@ -892,6 +938,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 +948,7 @@ "download_waiting_to_retry": "Αναμονή για επανάληψη", "downloading": "Γίνεται λήψη", "downloading_asset_filename": "Λήψη στοιχείου {filename}", + "downloading_from_icloud": "Λήψη από το iCloud", "downloading_media": "Λήψη πολυμέσων", "drop_files_to_upload": "Σύρετε αρχεία εδώ για να τα ανεβάσετε", "duplicates": "Διπλότυπα", @@ -929,11 +977,17 @@ "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_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": "Αυτός ο φάκελος είναι κενός", @@ -954,9 +1008,11 @@ "error_getting_places": "Σφάλμα κατά την ανάκτηση τοποθεσιών", "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 +1070,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 +1081,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 +1121,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 +1133,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 +1181,16 @@ "features": "Χαρακτηριστικά", "features_in_development": "Λειτουργίες υπό Ανάπτυξη", "features_setting_description": "Διαχειριστείτε τα χαρακτηριστικά της εφαρμογής", - "file_name": "Όνομα αρχείου", + "file_name": "Όνομα αρχείου: {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": "Διόρθωση λανθασμένης αντιστοίχισης", @@ -1137,12 +1200,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 +1242,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,8 +1315,11 @@ "ios_debug_info_processing_ran_at": "Η επεξεργασία εκτελέστηκε στις {dateTime}", "items_count": "{count, plural, one {# αντικείμενο} other {# αντικείμενα}}", "jobs": "Εργασίες", + "json_editor": "Επεξεργαστής JSON", + "json_error": "Σφάλμα JSON", "keep": "Διατήρηση", "keep_all": "Διατήρηση Όλων", + "keep_favorites": "Διατήρηση αγαπημένων", "keep_this_delete_others": "Διατήρηση αυτού, διαγραφή υπολοίπων", "kept_this_deleted_others": "Διατηρήθηκε αυτό το στοιχείο και διαγράφηκε/καν {count, plural, one {# στοιχείο} other {# στοιχεία}}", "keyboard_shortcuts": "Συντομεύσεις πληκτρολογίου", @@ -1343,10 +1414,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 +1497,8 @@ "minimize": "Ελαχιστοποίηση", "minute": "Λεπτό", "minutes": "Λεπτά", + "mirror_horizontal": "Οριζόντια", + "mirror_vertical": "Κάθετα", "missing": "Όσα Λείπουν", "mobile_app": "Εφαρμογή για κινητά", "mobile_app_download_onboarding_note": "Κατέβασε την συνοδευτική εφαρμογή για κινητά χρησιμοποιώντας τις παρακάτω επιλογές", @@ -1416,11 +1507,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 +1524,7 @@ "my_albums": "Τα άλμπουμ μου", "name": "Όνομα", "name_or_nickname": "Όνομα ή ψευδώνυμο", + "name_required": "Απαιτείται όνομα", "navigate": "Πλοηγηθείτε", "navigate_to_time": "Πλοηγηθείτε στο Χρόνο", "network_requirement_photos_upload": "Χρήση δεδομένων κινητής τηλεφωνίας για τη δημιουργία αντιγράφων ασφαλείας των φωτογραφιών", @@ -1454,20 +1549,23 @@ "next": "Επόμενο", "next_memory": "Επόμενη ανάμνηση", "no": "Όχι", + "no_actions_added": "Δεν έχουν προστεθεί ακόμα ενέργειες", "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": "Η τοποθεσία δεν έχει οριστεί", @@ -1563,6 +1661,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 +1686,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 +1769,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": "Η λειτουργία μόνο-για-ανάγνωση απενεργοποιήθηκε", @@ -1770,9 +1874,11 @@ "saved_settings": "Αποθηκευμένες ρυθμίσεις", "say_something": "Πείτε κάτι", "scaffold_body_error_occurred": "Παρουσιάστηκε σφάλμα", + "scan": "Σάρωση", "scan_all_libraries": "Σάρωση Όλων των Βιβλιοθηκών", "scan_library": "Σάρωση", "scan_settings": "Ρυθμίσεις Σάρωσης", + "scanning": "Σαρώνεται", "scanning_for_album": "Σάρωση για άλμπουμ...", "search": "Αναζήτηση", "search_albums": "Αναζήτηση άλμπουμ", @@ -1836,17 +1942,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 +1972,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": "Ορισμός", @@ -1982,6 +2094,7 @@ "show_password": "Εμφάνιση κωδικού", "show_person_options": "Εμφάνιση επιλογών ατόμου", "show_progress_bar": "Εμφάνιση γραμμής προόδου", + "show_schema": "Εμφάνιση σχήματος", "show_search_options": "Εμφάνιση επιλογών αναζήτησης", "show_shared_links": "Εμφάνιση κοινών συνδέσμων", "show_slideshow_transition": "Εμφάνιση μετάβασης παρουσίασης", @@ -2075,6 +2188,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 +2223,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 +2244,7 @@ "unhide_person": "Αναίρεση απόκρυψης ατόμου", "unknown": "Άγνωστο", "unknown_country": "Άγνωστη Χώρα", + "unknown_date": "Άγνωστη ημερομηνία", "unknown_year": "Άγνωστο Έτος", "unlimited": "Απεριόριστο", "unlink_motion_video": "Αποσυνδέστε το βίντεο κίνησης", @@ -2139,13 +2261,14 @@ "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(α) στοιχείο(α) στο διακομιστή;", @@ -2164,7 +2287,7 @@ "url": "URL", "usage": "Χρήση", "use_biometric": "Χρήση βιομετρικών στοιχείων", - "use_current_connection": "χρήση τρέχουσας σύνδεσης", + "use_current_connection": "Χρήση τρέχουσας σύνδεσης", "use_custom_date_range": "Χρήση προσαρμοσμένου εύρους ημερομηνιών", "user": "Χρήστης", "user_has_been_deleted": "Αυτός ο χρήστης έχει διεγραφεί.", @@ -2185,6 +2308,7 @@ "utilities": "Βοηθητικά προγράμματα", "validate": "Επικύρωση", "validate_endpoint_error": "Παρακαλώ εισάγετε ένα έγκυρο URL", + "validation_error": "Σφάλμα επικύρωσης", "variables": "Μεταβλητές", "version": "Έκδοση", "version_announcement_closing": "Ο φίλος σου, Alex", @@ -2196,6 +2320,7 @@ "video_hover_setting_description": "Προεπισκόπηση βίντεο όταν το ποντίκι βρίσκεται πάνω από το στοιχείο. Ακόμη και όταν είναι απενεργοποιημένη, η αναπαραγωγή μπορεί να ξεκινήσει τοποθετώντας το δείκτη του ποντικιού πάνω από το εικονίδιο αναπαραγωγής.", "videos": "Βίντεο", "videos_count": "{count, plural, one {# Βίντεο} other {# Βίντεο}}", + "videos_only": "Μόνο βίντεο", "view": "Προβολή", "view_album": "Προβολή Άλμπουμ", "view_all": "Προβολή Όλων", @@ -2216,6 +2341,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 +2351,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 473bd6f37b..a435c5986e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -104,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.", @@ -188,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", @@ -259,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", @@ -438,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}", @@ -501,6 +517,7 @@ "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", @@ -508,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.", @@ -552,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", @@ -603,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", @@ -740,13 +763,13 @@ "cleanup_confirm_prompt_title": "Remove from this device?", "cleanup_deleted_assets": "Moved {count} assets to device trash", "cleanup_deleting": "Moving to trash...", - "cleanup_filter_description": "Choose which types of assets to remove in the cleanup", "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 backed up assets found matching your criteria", + "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 photos and videos that have been backed up to the server with the selected cutoff date and filter options", - "cleanup_step4_summary": "{count} assets created before {date} are queued for removal from your device", + "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", @@ -833,6 +856,9 @@ "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", @@ -841,7 +867,7 @@ "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": "Remove photos and videos older than", + "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", @@ -925,6 +951,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", @@ -934,6 +961,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", @@ -966,9 +994,13 @@ "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_mode": "Editor mode", + "editor_confirm_reset_all_changes": "Are you sure you want to reset all changes?", + "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", @@ -987,11 +1019,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", @@ -1115,6 +1150,7 @@ "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...", @@ -1166,7 +1202,6 @@ "filetype": "Filetype", "filter": "Filter", "filter_description": "Conditions to filter the target assets", - "filter_options": "Filter options", "filter_people": "Filter people", "filter_places": "Filter places", "filters": "Filters", @@ -1180,7 +1215,7 @@ "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_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", @@ -1297,10 +1332,15 @@ "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_favorites_description": "Favorite assets will not be deleted from your device", + "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", @@ -1394,10 +1434,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", @@ -1459,6 +1517,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", @@ -1510,11 +1570,12 @@ "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", @@ -1539,6 +1600,7 @@ "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", @@ -1868,6 +1930,7 @@ "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_for": "Search for", "search_for_existing_person": "Search for existing person", "search_no_more_result": "No more results", @@ -2072,6 +2135,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", @@ -2148,6 +2213,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", @@ -2203,6 +2269,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", @@ -2227,11 +2294,11 @@ "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}", @@ -2246,7 +2313,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.", diff --git a/i18n/eo.json b/i18n/eo.json index 0967ef424b..9e8c8f8510 100644 --- a/i18n/eo.json +++ b/i18n/eo.json @@ -1 +1,419 @@ -{} +{ + "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_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 kaze ke la provizanto de OAuth ne subtenas PKCE (Proof Key for Code Exchange)", + "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 akcelado", + "transcoding_acceleration_api_description": "La API, kiu interagos kun via aparato por akceli 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_settings_description": "Administri transkodadon de videoj", + "trash_settings_description": "Administri agordojn pri rubaĵoj", + "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..c6d4106bd3 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 activos 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", @@ -70,9 +77,9 @@ "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\"", "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.", @@ -97,6 +104,8 @@ "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_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.", @@ -181,10 +190,21 @@ "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", @@ -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_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", @@ -467,10 +490,12 @@ "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_upload_assets": "Añadir recursos desde tu computadora y añadir a un álbum", "album_user_left": "Salida {album}", "album_user_removed": "Eliminado a {user}", "album_viewer_appbar_delete_confirm": "¿Estás seguro/a que quieres borrar este álbum de tu cuenta?", @@ -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.", @@ -524,10 +554,12 @@ "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?", + "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 archivo(s) de solo lectura, omitiendo", "asset_action_share_err_offline": "No se pudo obtener archivo(s) sin conexión, omitiendo", "asset_added_to_album": "Añadido al álbum", "asset_adding_to_album": "Añadiendo al álbum…", + "asset_created": "Activo creado", "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", @@ -549,7 +581,7 @@ "asset_troubleshoot": "Diagnóstico del elemento", "asset_uploaded": "Subido", "asset_uploading": "Subiendo…", - "asset_viewer_settings_subtitle": "Administra las configuracioens de tu visor de fotos", + "asset_viewer_settings_subtitle": "Administra las configuraciones 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}}", @@ -711,6 +743,8 @@ "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", @@ -722,6 +756,18 @@ "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": "¿Remover de este dispositivo?", + "cleanup_deleted_assets": "Moviendo {count} elementos del dispositivo a la papelera", + "cleanup_deleting": "Moviendo a la papelera...", + "cleanup_found_assets": "Se han encontrado {count} archivos respaldados", + "cleanup_found_assets_with_size": "Se encontraron {count} activos respaldados ({size})", + "cleanup_icloud_shared_albums_excluded": "Los álbumes compartidos de iCloud están excluidos del escaneo", + "cleanup_no_assets_found": "No se encontraron activos que coincidan con los criterios anteriores. Liberar espacio solo puede eliminar activos respaldados en el servidor", + "cleanup_preview_title": "{count} archivos a remover", + "cleanup_step3_description": "Busque activos 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", @@ -787,6 +833,7 @@ "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", @@ -801,17 +848,25 @@ "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", @@ -867,6 +922,7 @@ "deselect_all": "Deseleccionar Todo", "details": "Detalles", "direction": "Dirección", + "disable": "Desactivar", "disabled": "Deshabilitado", "disallow_edits": "Bloquear edición", "discord": "Discord", @@ -892,6 +948,7 @@ "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", @@ -901,6 +958,7 @@ "download_waiting_to_retry": "Esperando para reintentar", "downloading": "Descargando", "downloading_asset_filename": "Descargando archivo {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", @@ -929,16 +987,22 @@ "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", + "editor_confirm_reset_all_changes": "¿Seguro que quieres restablecer los cambios?", + "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 electrónico", "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 archivos de la basura de Immich.\n¡No podrás deshacer esta acción!", "enable": "Habilitar", "enable_backup": "Habilitar Copia de Seguridad", "enable_biometric_auth_description": "Introduce tu código PIN para habilitar la autentificación biométrica", @@ -952,11 +1016,14 @@ "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_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 activo", "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 activo", "errors": { "cannot_navigate_next_asset": "No puedes navegar al siguiente archivo", "cannot_navigate_previous_asset": "No puedes navegar al archivo anterior", @@ -979,7 +1046,7 @@ "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_get_people": "No se logró conseguir gente", "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", @@ -1014,6 +1081,7 @@ "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", @@ -1024,6 +1092,7 @@ "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", @@ -1063,6 +1132,7 @@ "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_unlink_account": "No se puede desvincular la cuenta", @@ -1074,8 +1144,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…", @@ -1120,14 +1192,16 @@ "features": "Características", "features_in_development": "Funciones en Desarrollo", "features_setting_description": "Administrar las funciones de la aplicación", - "file_name": "Nombre de archivo", + "file_name": "Nombre de archivo:{file_name}", "file_name_or_extension": "Nombre del archivo o extensión", "file_size": "Tamaño del archivo", "filename": "Nombre del archivo", "filetype": "Tipo de archivo", - "filter": "Filtros", + "filter": "Filtro", + "filter_description": "Condiciones para filtrar los activos 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 +1211,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", "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", @@ -1175,6 +1253,7 @@ "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.", @@ -1247,9 +1326,18 @@ "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_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", + "keeping": "Manteniendo: {items}", "kept_this_deleted_others": "Mantuvo este activo y eliminó {count, plural, one {# activo} other {# activos}}", "keyboard_shortcuts": "Atajos de teclado", "language": "Idioma", @@ -1343,10 +1431,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", @@ -1408,6 +1514,8 @@ "minimize": "Minimizar", "minute": "Minuto", "minutes": "Minutos", + "mirror_horizontal": "Horizontal", + "mirror_vertical": "Vertical", "missing": "Faltante", "mobile_app": "Aplicación Móvil", "mobile_app_download_onboarding_note": "Descarga la aplicación móvil utilizando las siguientes opciones", @@ -1416,11 +1524,14 @@ "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", @@ -1430,6 +1541,7 @@ "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", "network_requirement_photos_upload": "Usar datos móviles para crear una copia de seguridad de las fotos", @@ -1437,7 +1549,7 @@ "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", @@ -1454,20 +1566,24 @@ "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_message": "Haz clic para subir tu primera foto", "no_assets_to_show": "No hay elementos 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_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_location_set": "No se ha establecido ninguna ubicación", @@ -1481,6 +1597,7 @@ "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", @@ -1563,6 +1680,7 @@ "people": "Personas", "people_edits_count": "Editada {count, plural, one {# persona} other {# personas}}", "people_feature_description": "Explorar fotos y vídeos agrupados por personas", + "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", @@ -1587,11 +1705,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_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", @@ -1667,10 +1788,12 @@ "purchase_settings_server_activated": "La clave del producto del servidor la administra el administrador", "query_asset_id": "Consultar ID de elemento", "queue_status": "Poniendo en cola {count}/{total}", + "rate_asset": "Valorar activo", "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", @@ -1770,9 +1893,11 @@ "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 +1927,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", @@ -1836,17 +1962,23 @@ "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_trash_all": "Seleccionar eliminar todo", @@ -1938,7 +2070,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", @@ -1982,6 +2114,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 +2132,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", @@ -2075,6 +2210,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", @@ -2109,6 +2245,13 @@ "trash_page_select_assets_btn": "Seleccionar elementos", "trash_page_title": "Papelera ({count})", "trashed_items_will_be_permanently_deleted_after": "Los elementos en la papelera serán eliminados permanentemente tras {days, plural, one {# día} other {# días}}.", + "trigger": "Disparador", + "trigger_asset_uploaded": "Activo subido", + "trigger_asset_uploaded_description": "Se activa cuando se carga un nuevo activo", + "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,6 +2266,7 @@ "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", @@ -2139,13 +2283,14 @@ "unstack": "Desapilar", "unstack_action_prompt": "{count} desapilado(s)", "unstacked_assets_count": "Desapilado(s) {count, plural, one {# elemento} other {# elementos}}", + "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:", "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?", @@ -2164,7 +2309,7 @@ "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.", @@ -2185,6 +2330,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,6 +2342,7 @@ "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_all": "Ver todas", @@ -2216,6 +2363,8 @@ "viewer_stack_use_as_main_asset": "Usar como elemento 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", @@ -2224,13 +2373,26 @@ "welcome_to_immich": "Bienvenido a Immich", "width": "Ancho", "wifi_name": "Nombre Wi-Fi", - "workflow": "Flujo de trabajo", + "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 activos 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", + "zero_to_clear_rating": "presione 0 para borrar la calificación del activo", "zoom_image": "Acercar Imagen", "zoom_to_bounds": "Ajustar a los límites" } diff --git a/i18n/et.json b/i18n/et.json index b1db7e0466..899c785ac1 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", @@ -296,10 +316,10 @@ "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", @@ -690,7 +722,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 +743,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 +756,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", @@ -787,6 +833,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 +848,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 +922,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 +948,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 +958,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 +987,17 @@ "edit_tag": "Muuda silti", "edit_title": "Muuda pealkirja", "edit_user": "Muuda kasutajat", + "edit_workflow": "Muuda töövoogu", "editor": "Muutja", "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_confirm_reset_all_changes": "Kas oled kindel, et soovid kõik muudatused tühistada?", + "editor_flip_horizontal": "Pööra horisontaalselt", + "editor_flip_vertical": "Pööra 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 +1016,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 +1081,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 +1092,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 +1132,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 +1144,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...", @@ -1120,14 +1192,16 @@ "features": "Funktsioonid", "features_in_development": "Arendusjärgus olevad funktsioonid", "features_setting_description": "Halda rakenduse funktsioone", - "file_name": "Failinimi", + "file_name": "Failinimi: {file_name}", "file_name_or_extension": "Failinimi või -laiend", "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 +1211,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 +1253,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 +1326,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 +1431,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 +1514,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 +1524,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 +1541,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", @@ -1454,20 +1566,24 @@ "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", @@ -1563,6 +1679,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 +1704,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 +1787,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", @@ -1770,9 +1892,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 +1926,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 +1961,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 +2113,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 +2131,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", @@ -2075,6 +2209,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 +2244,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 +2265,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,13 +2282,14 @@ "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?", @@ -2164,7 +2308,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 +2329,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 +2341,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 +2362,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 +2372,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..e246086094 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": "شهر", diff --git a/i18n/fi.json b/i18n/fi.json index 3eab7b3df7..fb048da219 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,7 @@ "features": "Ominaisuudet", "features_in_development": "Kehityksessä olevat ominaisuudet", "features_setting_description": "Hallitse sovelluksen ominaisuuksia", - "file_name": "Tiedoston nimi", + "file_name": "Tiedoston nimi: {file_name}", "file_name_or_extension": "Tiedostonimi tai tiedostopääte", "file_size": "Tiedostokoko", "filename": "Tiedostonimi", @@ -1126,6 +1185,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 +1226,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 +1306,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 +1409,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 +1510,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 +1535,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 +1551,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", @@ -1583,11 +1671,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", @@ -1832,7 +1923,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 +1936,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 +2072,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 +2090,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 +2202,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 +2221,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 +2238,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 +2264,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 +2285,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 +2297,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 +2318,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..b79ccf2660 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", @@ -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": "Progressive", + "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", @@ -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", @@ -575,7 +607,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 +623,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 +743,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 +756,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", @@ -787,6 +833,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 +848,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 le dernier…", + "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 +922,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 +948,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 +958,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 +987,17 @@ "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_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 +1016,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 +1081,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 +1092,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 +1132,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 +1144,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...", @@ -1120,14 +1192,16 @@ "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": "Nom du fichier : {file_name}", "file_name_or_extension": "Nom du fichier ou extension", "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 +1211,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 +1253,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 +1326,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 +1431,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,6 +1514,8 @@ "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", @@ -1416,11 +1524,14 @@ "monthly_title_text_date_format": "MMMM y", "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 +1541,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 +1566,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,6 +1597,7 @@ "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", @@ -1563,6 +1680,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 +1705,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 +1788,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é", @@ -1770,9 +1893,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 +1927,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 +1962,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 +2114,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 +2132,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", @@ -2075,6 +2210,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", @@ -2109,6 +2245,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 +2266,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,13 +2283,14 @@ "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 ?", @@ -2185,6 +2330,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 +2342,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 +2363,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 +2373,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..2f5638e4d8 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", @@ -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", @@ -591,7 +623,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 +743,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 +756,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í", @@ -787,6 +833,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 +848,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 +922,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 +948,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 +958,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 +987,17 @@ "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_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 +1016,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 +1081,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 +1092,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 +1132,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 +1144,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...", @@ -1120,14 +1192,16 @@ "features": "Gnéithe", "features_in_development": "Gnéithe i bhForbairt", "features_setting_description": "Bainistigh gnéithe an aip", - "file_name": "Ainm comhaid", + "file_name": "Ainm comhaid: {file_name}", "file_name_or_extension": "Ainm comhaid nó síneadh", "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 +1211,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 +1253,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 +1326,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 +1431,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 +1514,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 +1524,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 +1541,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 +1566,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,6 +1597,7 @@ "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", @@ -1563,6 +1680,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 +1705,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 +1788,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", @@ -1770,9 +1893,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 +1927,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 +1962,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 +2114,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 +2132,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", @@ -2075,6 +2210,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 +2245,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 +2266,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,13 +2283,14 @@ "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í?", @@ -2164,7 +2309,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 +2330,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 +2342,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 +2363,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 +2373,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..9127d04978 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,13 @@ "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_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 +41,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 +74,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 +82,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.", @@ -101,6 +111,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 +119,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", @@ -175,7 +188,11 @@ "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_settings": "Mantemento", + "maintenance_settings_description": "Poñer Immich en modo mantemento.", + "maintenance_start": "Comezar modo de mantemento", + "maintenance_start_error": "Erro ao iniciar o modo de mantemento.", "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", @@ -265,10 +282,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 +302,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", @@ -401,6 +424,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", @@ -448,6 +473,7 @@ "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", @@ -469,6 +495,7 @@ "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", @@ -505,10 +532,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", @@ -633,6 +662,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 +721,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,6 +731,7 @@ "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", "clear": "Limpar", @@ -721,6 +754,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 +799,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,6 +814,7 @@ "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...", @@ -845,6 +881,7 @@ "deselect_all": "Deseleccionar todo", "details": "Detalles", "direction": "Dirección", + "disable": "Desactivar", "disabled": "Desactivado", "disallow_edits": "Non permitir edicións", "discord": "Discord", @@ -870,6 +907,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", @@ -907,11 +945,10 @@ "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", "email": "Correo electrónico", "email_notifications": "Notificacións por correo electrónico", "empty_folder": "Este cartafol está baleiro", @@ -969,6 +1006,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 +1029,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 +1040,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", @@ -1051,8 +1091,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 +1126,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", @@ -1101,8 +1145,10 @@ "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 +1158,13 @@ "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", + "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 +1190,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,6 +1270,8 @@ "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_all": "Conservar Todo", "keep_this_delete_others": "Conservar este, eliminar outros", @@ -1241,6 +1294,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 +1316,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 +1368,11 @@ "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_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_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.", @@ -1380,11 +1441,13 @@ "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_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 +1457,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,6 +1482,7 @@ "next": "Seguinte", "next_memory": "Seguinte recordo", "no": "Non", + "no_actions_added": "Non hai accións engadidas polo momento", "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.", @@ -1427,13 +1492,16 @@ "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", @@ -1493,6 +1561,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 +1594,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,6 +1619,8 @@ "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", @@ -1798,17 +1870,22 @@ "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_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 +1901,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 +2021,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", @@ -2017,6 +2098,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", @@ -2049,6 +2131,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 +2149,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", @@ -2096,13 +2186,14 @@ "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?", @@ -2142,6 +2233,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", @@ -2157,6 +2249,7 @@ "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,13 +2265,29 @@ "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}}", diff --git a/i18n/gsw.json b/i18n/gsw.json index b9a0ebcab7..0615722e34 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", diff --git a/i18n/he.json b/i18n/he.json index 29acf7a029..76762175df 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": "שגיאה בעדכון התיאור", @@ -1128,7 +1144,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 +1229,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 +1266,7 @@ "language_setting_description": "בחר את השפה המועדפת עליך", "large_files": "קבצים גדולים", "last": "אחרון", + "last_months": "{count, plural, one {החודש האחרון} other {# החודשים האחרונים}}", "last_seen": "נראה לאחרונה", "latest_version": "גרסה עדכנית ביותר", "latitude": "קו רוחב", @@ -1280,6 +1298,7 @@ "local": "מקומי", "local_asset_cast_failed": "לא ניתן לשדר תמונה שלא הועלתה לשרת", "local_assets": "תמונות מקומיות", + "local_id": "ID מקומי", "local_media_summary": "סיכום של מדיה מקומית", "local_network": "רשת מקומית", "local_network_sheet_info": "היישום יתחבר לשרת דרך הכתובת הזאת כאשר משתמשים ברשת האינטרנט האלחוטי שמצוינת", @@ -1331,10 +1350,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 +1424,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 +1470,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,6 +1488,7 @@ "no_results_description": "נסה להשתמש במילה נרדפת או במילת מפתח יותר כללית", "no_shared_albums_message": "צור אלבום כדי לשתף תמונות וסרטונים עם אנשים ברשת שלך", "no_uploads_in_progress": "אין העלאות בתהליך", + "not_allowed": "לא מורשה", "not_available": "לא רלוונטי", "not_in_any_album": "לא בשום אלבום", "not_selected": "לא נבחרו", @@ -1507,6 +1537,7 @@ "other_variables": "משתנים אחרים", "owned": "בבעלות", "owner": "בעלים", + "page": "דף", "partner": "שותף", "partner_can_access": "{partner} יכול/ה לגשת", "partner_can_access_assets": "כל התמונות והסרטונים שלך פרט לאלו שבארכיון ושנמחקו", @@ -1569,6 +1600,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 הוגדר בהצלחה", @@ -1836,6 +1869,8 @@ "server_offline": "השרת מנותק", "server_online": "החיבור לשרת פעיל", "server_privacy": "פרטיות השרת", + "server_restarting_description": "הדף יתרענן בעוד רגע.", + "server_restarting_title": "השרת מופעל מחדש", "server_stats": "סטטיסטיקות שרת", "server_update_available": "עדכון שרת זמין", "server_version": "גרסת שרת", @@ -1959,6 +1994,7 @@ "show_slideshow_transition": "הצג מעבר מצגת", "show_supporter_badge": "תג תומך", "show_supporter_badge_description": "הצג תג תומך", + "show_text_recognition": "הצג זיהוי טקסט", "show_text_search_menu": "הצג תפריט חיפוש טקסט", "shuffle": "ערבוב", "sidebar": "סרגל צד", @@ -2029,6 +2065,7 @@ "tags": "תגים", "tap_to_run_job": "לחץ על מנת להפעיל משימה", "template": "תבנית", + "text_recognition": "זיהוי טקסט", "theme": "ערכת נושא", "theme_selection": "בחירת ערכת נושא", "theme_selection_description": "הגדר אוטומטית את ערכת הנושא לבהיר או כהה בהתבסס על העדפת המערכת של הדפדפן שלך", @@ -2049,6 +2086,7 @@ "third_party_resources": "משאבי צד שלישי", "time": "זמן", "time_based_memories": "זכרונות מבוססי זמן", + "time_based_memories_duration": "מספר השניות להצגת כל תמונה.", "timeline": "ציר זמן", "timezone": "אזור זמן", "to_archive": "העבר לארכיון", @@ -2060,6 +2098,7 @@ "to_select": "לבחור", "to_trash": "אשפה", "toggle_settings": "החלף מצב הגדרות", + "toggle_theme_description": "הפעלה/כיבוי של ערכת נושא", "total": "סה\"כ", "total_usage": "שימוש כולל", "trash": "אשפה", @@ -2113,8 +2152,7 @@ "updated_at": "עודכן", "updated_password": "סיסמה עודכנה", "upload": "העלאה", - "upload_action_prompt": "{count} נוספו לתור להעלאה", - "upload_concurrency": "בו-זמניות של העלאה", + "upload_concurrency": "מספר העלאות במקביל", "upload_details": "פרטי העלאה", "upload_dialog_info": "האם ברצונך לגבות את התמונות שנבחרו לשרת?", "upload_dialog_title": "העלאת תמונה", @@ -2168,6 +2206,7 @@ "view_album": "הצג אלבום", "view_all": "הצג הכל", "view_all_users": "הצג את כל המשתמשים", + "view_asset_owners": "הצג את בעלי התמונות", "view_details": "הצג פרטים", "view_in_timeline": "ראה בציר הזמן", "view_link": "הצג קישור", @@ -2184,10 +2223,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..f583ee2411 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} डाला गया", @@ -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": "समूह ने लोगों में चेहरों का पता लगाया। यह चरण फेस डिटेक्शन पूरा होने के बाद चलता है। \"सभी\" चेहरों को (पुनः) समूहित करता है। \"लापता\" कतार में वे चेहरे हैं जिनके लिए कोई व्यक्ति नियुक्त नहीं है।", @@ -101,6 +112,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 +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": "संग्रह विवरण", @@ -914,8 +927,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": "यह फ़ोल्डर खाली है", @@ -2122,7 +2133,6 @@ "updated_at": "अपडेट किया गया", "updated_password": "अद्यतन पासवर्ड", "upload": "डालना", - "upload_action_prompt": "अपलोड के लिए {count} कतार में", "upload_concurrency": "समवर्ती अपलोड करें", "upload_details": "विवरण अपलोड करें", "upload_dialog_info": "क्या आप चुने हुए एसेट का सर्वर पर बैकअप लेना चाहते हैं?", @@ -2198,7 +2208,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..40dcdc3fe4 100644 --- a/i18n/hr.json +++ b/i18n/hr.json @@ -908,8 +908,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", @@ -2080,7 +2078,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 +2155,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..7d91afee76 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,7 +265,7 @@ "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", @@ -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,13 +348,13 @@ "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": "Egyéni 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_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.", @@ -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,24 +431,24 @@ "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_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.", @@ -437,13 +457,13 @@ "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 +482,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 +501,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,6 +510,7 @@ "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", @@ -499,16 +522,16 @@ "allowed": "Engedélyezett", "alt_text_qr_code": "QR kód kép", "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 +547,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 +565,7 @@ "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_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 +575,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,9 +599,9 @@ "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", @@ -591,7 +616,7 @@ "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 +626,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 +652,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 +686,17 @@ "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", "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 +704,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 +727,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 +744,29 @@ "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_icloud_shared_albums_excluded": "Megosztott iCloud albumok nem kerülnek átnézésre", + "cleanup_no_assets_found": "Nincs feltöltött elem ezekkel a kritériumokkal", + "cleanup_preview_title": "Törlendő elemek ({count})", + "cleanup_step3_description": "Szerverre feltöltött képek és videók 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", + "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ó", @@ -749,7 +787,7 @@ "common_create_new_album": "Új album létrehozása", "completed": "Kész", "confirm": "Jóváhagy", - "confirm_admin_password": "Admin Jelszó Újból", + "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 +803,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,36 +839,44 @@ "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": "Ennél régebbi fotók és videók eltávolítása", + "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", @@ -840,21 +887,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 +913,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 +933,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 +949,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 +976,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", @@ -953,9 +1009,11 @@ "error_getting_places": "Hiba a helyek 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 +1071,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 +1082,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 +1118,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 +1134,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 +1147,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 +1159,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 +1179,19 @@ "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": "Fájlnév: {file_name}", "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 +1201,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 +1226,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 +1236,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 +1258,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 +1281,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 +1306,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,8 +1316,11 @@ "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_all": "Összes megtartása", + "keep_favorites": "Kedvencek megtartása", "keep_this_delete_others": "Ennek a meghagyása, a többi törlése", "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", @@ -1254,11 +1329,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 +1344,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 +1356,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 +1379,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", @@ -1339,10 +1415,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,", + "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 +1457,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 +1468,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 +1498,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 +1508,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 +1525,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 +1535,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 +1550,28 @@ "next": "Következő", "next_memory": "Következő emlék", "no": "Nem", + "no_actions_added": "Még nincsenek műveletek", "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", @@ -1481,21 +1584,21 @@ "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)", + "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 +1632,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 +1662,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 +1687,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 +1704,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 +1717,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 +1729,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 +1760,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 +1770,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", @@ -1680,7 +1789,7 @@ "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 +1804,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 +1824,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 +1846,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 +1870,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": "Scan", + "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 +1901,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 +1919,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 +1939,47 @@ "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_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 +2025,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 +2034,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 +2080,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 +2113,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", @@ -2019,8 +2139,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 +2152,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 +2178,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", @@ -2072,7 +2192,7 @@ "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", "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 +2214,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 +2246,38 @@ "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_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 +2309,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 +2321,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 +2337,40 @@ "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", + "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..2f85f23912 100644 --- a/i18n/id.json +++ b/i18n/id.json @@ -5,8 +5,10 @@ "acknowledge": "Mengerti", "action": "Tindakan", "action_common_update": "Perbarui", + "action_description": "Sebuah kelompok perbuatan untuk melakukan suatu aksi pada aset-aset yang terfiltrasi", "actions": "Tindakan", "active": "Aktif", + "active_count": "Aktif: {count}", "activity": "Aktivitas", "activity_changed": "Aktivitas {enabled, select, true {diaktifkan} other {dinonaktifkan}}", "add": "Tambahkan", @@ -14,9 +16,14 @@ "add_a_location": "Tambahkan lokasi", "add_a_name": "Tambahkan nama", "add_a_title": "Tambahkan judul", + "add_action": "Tambah aksi", + "add_action_description": "Klik untuk menambahkan aksi yang akan dilakukan", + "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", @@ -98,7 +106,7 @@ "image_preview_title": "Pengaturan Pratinjau", "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 +120,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", @@ -180,10 +189,11 @@ "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_settings": "Pemeliharaan", - "maintenance_settings_description": "Setel mode pemeliharaan Immich", + "maintenance_settings_description": "Setel mode pemeliharaan Immich.", "maintenance_start": "Mulai mode pemeliharaan", "maintenance_start_error": "Gagal memulai mode pemeliharaan.", "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", @@ -273,10 +283,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 +303,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 +374,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 +392,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 +425,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", @@ -426,11 +444,11 @@ "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 +457,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 +474,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,6 +497,7 @@ "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", @@ -512,10 +534,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", @@ -640,6 +664,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 +723,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 +733,11 @@ "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?", "clear": "Hapus", "clear_all": "Hapus semua", "clear_all_recent_searches": "Hapus semua pencarian terakhir", @@ -720,14 +750,15 @@ "client_cert_import_success_msg": "Sertifikat klien telah diimpor", "client_cert_invalid_msg": "File sertifikat tidak valid atau kata sandi salah", "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 +803,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,6 +818,7 @@ "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...", @@ -852,6 +885,7 @@ "deselect_all": "Batalkan semua pilihan", "details": "Detail", "direction": "Arah", + "disable": "Nonaktifkan", "disabled": "Dinonaktifkan", "disallow_edits": "Jangan izinkan penyuntingan", "discord": "Discord", @@ -914,11 +948,10 @@ "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", "email": "Surel", "email_notifications": "Notifikasi surel", "empty_folder": "Folder ini kosong", @@ -976,6 +1009,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 +1032,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 +1043,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 +1083,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 +1095,10 @@ "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" }, + "exclusion_pattern": "Pola pengecualian", "exif": "EXIF", "exif_bottom_sheet_description": "Tambahkan Deskripsi...", "exif_bottom_sheet_description_error": "Galat saat memperbaharui deskripsi", @@ -1090,6 +1129,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 +1142,16 @@ "features": "Fitur", "features_in_development": "Fitur dalam Pengembangan", "features_setting_description": "Kelola fitur aplikasi", - "file_name": "Nama berkas", + "file_name": "Nama berkas: {file_name}", "file_name_or_extension": "Nama berkas atau ekstensi", "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 +1161,13 @@ "folders_feature_description": "Menjelajahi tampilan folder untuk foto dan video pada sistem file", "forgot_pin_code_question": "Lupa PIN?", "forward": "Maju", + "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 +1193,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 +1247,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,6 +1273,8 @@ "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_all": "Simpan Semua", "keep_this_delete_others": "Pertahankan ini, hapus lainnya", @@ -1236,6 +1287,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 +1297,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 +1319,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 +1371,17 @@ "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_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_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", @@ -1380,10 +1444,13 @@ "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_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 +1460,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,12 +1478,14 @@ "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_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.", @@ -1425,12 +1495,16 @@ "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,6 +1515,7 @@ "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", + "not_allowed": "Tidak diperbolehkan", "not_available": "T/T", "not_in_any_album": "Tidak ada dalam album apa pun", "not_selected": "Belum dipilih", @@ -1489,6 +1564,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 +1597,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 +1622,16 @@ "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", "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 +1704,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", @@ -1792,17 +1875,22 @@ "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_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 +1906,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 +2026,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", @@ -2011,6 +2103,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", @@ -2031,6 +2124,7 @@ "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 +2136,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 +2154,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", @@ -2089,13 +2191,14 @@ "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?", @@ -2135,6 +2238,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", @@ -2150,6 +2254,7 @@ "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 +2270,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..a16f23e455 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", diff --git a/i18n/it.json b/i18n/it.json index fbc1b32e36..a11879d57f 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", @@ -181,10 +188,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 +228,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 +245,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.", @@ -303,15 +321,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 +449,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 +488,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,9 +508,10 @@ "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", @@ -516,7 +540,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 +548,58 @@ "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_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 +617,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 +637,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 +690,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 +737,30 @@ "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_icloud_shared_albums_excluded": "Gli Album Condivisi di iCloud sono esclusi dalla ricerca", + "cleanup_no_assets_found": "Nessuna risorsa già salvata corrisponde ai criteri richiesti", + "cleanup_preview_title": "Risorse da rimuovere ({count})", + "cleanup_step3_description": "Ricerca foto e video che sono stati già salvati sul server e che corrispondono alle opzioni di ricerca", + "cleanup_step4_summary": "{count} risorse create prima del {date} sono in coda per la rimozione dal dispositivo", + "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", @@ -751,9 +790,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 +826,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": "Rimuovi foto e video più vecchi del", + "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 +877,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 +908,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 +926,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 +941,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 +949,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 +980,17 @@ "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_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 +1007,47 @@ "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_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 +1056,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 +1099,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 +1124,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 +1136,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...", @@ -1109,7 +1173,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 +1184,16 @@ "features": "Funzionalità", "features_in_development": "Funzionalità in fase di sviluppo", "features_setting_description": "Gestisci le funzionalità dell'app", - "file_name": "Nome file", + "file_name": "Nome file: {file_name}", "file_name_or_extension": "Nome file o estensione", "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 +1203,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 +1230,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 +1245,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 +1296,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 +1318,13 @@ "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_all": "Tieni tutto", + "keep_favorites": "Mantieni i favoriti", "keep_this_delete_others": "Tieni questo, elimina gli altri", - "kept_this_deleted_others": "Mantenuto questo asset ed eliminati {count, plural, one {# asset} other {# assets}}", + "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 +1348,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 +1417,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 +1454,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 +1480,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 +1500,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 +1510,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 +1527,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,29 +1552,33 @@ "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", @@ -1485,7 +1587,7 @@ "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", + "note_apply_storage_label_to_previously_uploaded assets": "Nota: Per aggiungere l'etichetta dell'archiviazione alle risorse caricate 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.", @@ -1563,14 +1665,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 +1690,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,21 +1771,23 @@ "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_searches": "Ricerche recenti", @@ -1702,11 +1810,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 +1833,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 +1860,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 +1878,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 +1912,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 +1947,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 +2020,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 +2072,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 +2083,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 +2099,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", @@ -2015,7 +2133,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", @@ -2054,7 +2172,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 +2180,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 +2193,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 +2217,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 +2249,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 +2265,39 @@ "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_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 +2313,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 +2325,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 +2356,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..85d4183b28 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": "項目に名前のついていない人物の顔があります", @@ -637,6 +684,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 +743,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": "全ての最近の検索をクリア", @@ -725,6 +788,7 @@ "collapse_all": "全て展開", "color": "カラー", "color_theme": "カラーテーマ", + "command": "コマンド", "comment_deleted": "コメントが削除されました", "comment_options": "コメント設定", "comments_and_likes": "コメントといいね", @@ -769,6 +833,7 @@ "create_album": "アルバムを作成", "create_album_page_untitled": "無題のタイトル", "create_api_key": "APIキーを作成", + "create_first_workflow": "初めてのワークフローを作成", "create_library": "ライブラリを作成", "create_link": "リンクを作る", "create_link_to_share": "共有リンクを作る", @@ -783,17 +848,23 @@ "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": "写真を保持する期間:", "daily_title_text_date": "MM DD, EE", "daily_title_text_date_year": "yyyy MM DD, EE", "dark": "ダークモード", @@ -819,9 +890,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 +920,7 @@ "deselect_all": "すべての選択を解除", "details": "詳細", "direction": "方向", + "disable": "無効化", "disabled": "無効", "disallow_edits": "編集を許可しない", "discord": "Discord", @@ -874,6 +946,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 +956,7 @@ "download_waiting_to_retry": "リトライ中", "downloading": "ダウンロード中", "downloading_asset_filename": "写真/動画 {filename} をダウンロード中", + "downloading_from_icloud": "iCloudからダウンロード", "downloading_media": "ダウンロード中", "drop_files_to_upload": "ファイルをドロップしてアップロード", "duplicates": "重複", @@ -911,16 +985,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_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 +1014,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 +1079,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 +1090,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 +1130,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 +1142,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 +1177,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 +1190,16 @@ "features": "機能", "features_in_development": "開発中の機能", "features_setting_description": "アプリの機能を管理する", - "file_name": "ファイル名", + "file_name": "ファイル名: {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": "間違った一致を修正", @@ -1118,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": "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 +1244,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 +1276,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 +1292,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 +1324,17 @@ "ios_debug_info_processing_ran_at": "処理実行済み: {dateTime}", "items_count": "{count, plural, one {#個} other {#個}}の項目", "jobs": "ジョブ", + "json_editor": "JSONエディター", + "json_error": "JSONエラー", "keep": "保持", + "keep_albums": "アルバムを保持", "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 +1376,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 +1428,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 +1511,8 @@ "minimize": "最小化", "minute": "分", "minutes": "分", + "mirror_horizontal": "水平", + "mirror_vertical": "垂直", "missing": "欠落", "mobile_app": "モバイルアプリ", "mobile_app_download_onboarding_note": "以下のオプションを使用してコンパニオンモバイルアプリをダウンロードしてください", @@ -1395,11 +1521,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 +1538,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 +1563,8 @@ "next": "次", "next_memory": "次のメモリー", "no": "いいえ", + "no_actions_added": "アクションがありません", + "no_albums_found": "アルバムが見つかりません", "no_albums_message": "アルバムを作成して写真や動画を整理しましょう", "no_albums_with_name_yet": "この名前のアルバムはまだないようです。", "no_albums_yet": "まだアルバムがないようです。", @@ -1442,11 +1574,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,6 +1594,7 @@ "no_results_description": "同義語やより一般的なキーワードを試してください", "no_shared_albums_message": "アルバムを作成して写真や動画を共有しましょう", "no_uploads_in_progress": "アップロードは行われていません", + "none": "なし", "not_allowed": "許可されていません", "not_available": "適用なし", "not_in_any_album": "どのアルバムにも入っていない", @@ -1509,6 +1644,7 @@ "other_variables": "その他の変数", "owned": "所有中", "owner": "オーナー", + "page": "ページ", "partner": "パートナー", "partner_can_access": "{partner} がアクセスできます", "partner_can_access_assets": "アーカイブ済みのものと削除済みのものを除いた全ての写真と動画", @@ -1541,6 +1677,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 +1702,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,6 +1785,7 @@ "purchase_settings_server_activated": "サーバーのプロダクトキーは管理者に管理されています", "query_asset_id": "順番待ちの項目ID", "queue_status": "順番待ち中 {count}/{total}", + "rate_asset": "項目を評価する", "rating": "星での評価", "rating_clear": "評価を取り消す", "rating_count": "星{count, plural, one {#つ} other {#つ}}", @@ -1748,9 +1889,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 +1923,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 +1958,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 +2110,7 @@ "show_password": "パスワードを表示", "show_person_options": "人物設定を表示", "show_progress_bar": "プログレスバーを表示", + "show_schema": "スキーマを表示", "show_search_options": "検索オプションを表示", "show_shared_links": "共有リンクを表示", "show_slideshow_transition": "スライドショーのトランジションを表示", @@ -1977,6 +2128,8 @@ "skip_to_folders": "フォルダへスキップ", "skip_to_tags": "タグへスキップ", "slideshow": "スライドショー", + "slideshow_repeat": "スライドショーを繰り返す", + "slideshow_repeat_description": "スライドショーが終わったら始めに戻ります", "slideshow_settings": "スライドショー設定", "sort_albums_by": "この順序でアルバムをソート…", "sort_created": "作成日", @@ -2069,6 +2222,7 @@ "to_select": "選択", "to_trash": "ゴミ箱", "toggle_settings": "設定をトグル", + "toggle_theme_description": "テーマを切り替え", "total": "合計", "total_usage": "総使用量", "trash": "ゴミ箱", @@ -2086,6 +2240,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 +2261,7 @@ "unhide_person": "人物の非表示を解除", "unknown": "不明", "unknown_country": "不明な国", + "unknown_date": "不明な日付", "unknown_year": "不明な年", "unlimited": "無制限", "unlink_motion_video": "モーションビデオのリンクを解除", @@ -2116,13 +2278,14 @@ "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": "選択した項目のバックアップをしますか?", @@ -2162,6 +2325,7 @@ "utilities": "ユーティリティ", "validate": "認証", "validate_endpoint_error": "有効なURLを入力してください", + "validation_error": "バリデーションエラー", "variables": "変数", "version": "バージョン", "version_announcement_closing": "あなたの友人、Alex", @@ -2173,10 +2337,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 +2358,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..ae367460c5 100644 --- a/i18n/ka.json +++ b/i18n/ka.json @@ -7,6 +7,7 @@ "action_common_update": "განაახლე", "actions": "ქმედებები", "active": "აქტიური", + "active_count": "aქტიური: {count}", "activity": "აქტივობა", "activity_changed": "აქტივობა {enabled, select, true {ჩართული} other {გამორთული}}", "add": "დაამატე", @@ -14,9 +15,11 @@ "add_a_location": "დაამატე ადგილი", "add_a_name": "დაამატე სახელი", "add_a_title": "დაასათაურე", + "add_action": "დაამატე მოქმედება", "add_birthday": "დაბადების დღის დამატება", "add_endpoint": "ბოლოწერტილის დამატება", "add_exclusion_pattern": "დაამატე გამონაკლისი ნიმუში", + "add_filter": "დაამატე ფილტრი", "add_location": "დაამატე ადგილი", "add_more_users": "დაამატე მომხმარებლები", "add_partner": "დაამატე პარტნიორი", @@ -36,7 +39,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 +48,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,8 +79,10 @@ "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", @@ -85,26 +91,53 @@ }, "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 +146,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 +170,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 +189,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..ec7c174e69 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,258 @@ "add_a_location": "ಸ್ಥಳವನ್ನು ಸೇರಿಸಿ", "add_a_name": "ಹೆಸರನ್ನು ಸೇರಿಸಿ", "add_a_title": "ಶೀರ್ಷಿಕೆಯನ್ನು ಸೇರಿಸಿ", + "add_action": "ಕ್ರಿಯೆಯನ್ನು ಸೇರಿಸಿ", + "add_action_description": "ನಿರ್ವಹಿಸಲು ಕ್ರಿಯೆಯನ್ನು ಸೇರಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ", "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": "ನಿರ್ವಾಹಕ ಬಳಕೆದಾರ", + "authentication_settings": "ದೃಢೀಕರಣ ಸೆಟ್ಟಿಂಗ್‌ಗಳು", + "authentication_settings_description": "ಪಾಸ್‌ವರ್ಡ್, ಒಔತ್ ಮತ್ತು ಇತರ ದೃಢೀಕರಣ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ನಿರ್ವಹಿಸಿ", + "authentication_settings_disable_all": "ನೀವು ಎಲ್ಲಾ ಲಾಗಿನ್ ವಿಧಾನಗಳನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ? ಲಾಗಿನ್ ಅನ್ನು ಸಂಪೂರ್ಣವಾಗಿ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗುತ್ತದೆ.", + "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": "ಲಾಗಿನ್ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ", + "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_quality_description": "1-100 ರವರೆಗಿನ ಪೂರ್ಣ-ಗಾತ್ರದ ಚಿತ್ರದ ಗುಣಮಟ್ಟ. ಹೆಚ್ಚಿನದು ಉತ್ತಮ, ಆದರೆ ದೊಡ್ಡ ಫೈಲ್‌ಗಳನ್ನು ಉತ್ಪಾದಿಸುತ್ತದೆ.", + "image_fullsize_title": "ಪೂರ್ಣ-ಗಾತ್ರದ ಚಿತ್ರ ಸೆಟ್ಟಿಂಗ್‌ಗಳು", + "image_prefer_embedded_preview": "ಎಂಬೆಡ್ ಮಾಡಿದ ಪೂರ್ವವೀಕ್ಷಣೆಗೆ ಆದ್ಯತೆ ನೀಡಿ", + "image_prefer_wide_gamut": "ವಿಶಾಲ ವ್ಯಾಪ್ತಿಗೆ ಆದ್ಯತೆ ನೀಡಿ", + "image_preview_quality_description": "1-100 ವರೆಗಿನ ಪೂರ್ವವೀಕ್ಷಣೆ ಗುಣಮಟ್ಟ. ಹೆಚ್ಚಿನದು ಉತ್ತಮ, ಆದರೆ ದೊಡ್ಡ ಫೈಲ್‌ಗಳನ್ನು ಉತ್ಪಾದಿಸುತ್ತದೆ ಮತ್ತು ಅಪ್ಲಿಕೇಶನ್ ಪ್ರತಿಕ್ರಿಯೆಯನ್ನು ಕಡಿಮೆ ಮಾಡುತ್ತದೆ. ಕಡಿಮೆ ಮೌಲ್ಯವನ್ನು ಹೊಂದಿಸುವುದು ಯಂತ್ರ ಕಲಿಕೆಯ ಗುಣಮಟ್ಟದ ಮೇಲೆ ಪರಿಣಾಮ ಬೀರಬಹುದು.", + "image_preview_title": "ಪೂರ್ವವೀಕ್ಷಣೆ ಸೆಟ್ಟಿಂಗ್‌ಗಳು", + "image_quality": "ಗುಣಮಟ್ಟ", + "image_resolution": "ರೆಸಲ್ಯೂಶನ್", + "image_settings": "ಚಿತ್ರ ಸೆಟ್ಟಿಂಗ್‌ಗಳು", + "image_settings_description": "ರಚಿಸಲಾದ ಚಿತ್ರಗಳ ಗುಣಮಟ್ಟ ಮತ್ತು ರೆಸಲ್ಯೂಶನ್ ಅನ್ನು ನಿರ್ವಹಿಸಿ", + "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_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_timeout": "ವಿನಂತಿ ಅವಧಿ ಮೀರಿದೆ", + "machine_learning_clip_model": "CLIP ಮಾದರಿ", + "machine_learning_duplicate_detection": "ನಕಲು ಪತ್ತೆ", + "machine_learning_duplicate_detection_enabled": "ನಕಲು ಪತ್ತೆಹಚ್ಚುವಿಕೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ", + "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_ocr": "ಓಸಿಆರ್", + "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_recognition_score": "ಕನಿಷ್ಠ ಡಿಟೆಕ್ಷನ್ ಅಂಕ", + "machine_learning_ocr_model": "OCR ಮಾಡೆಲ್", + "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_settings": "ನಿರ್ವಹಣೆ", + "maintenance_settings_description": "ಇಮ್ಮಿಚ್ ಅನ್ನು ನಿರ್ವಹಣಾ ಕ್ರಮಕ್ಕೆ ಇರಿಸಿ.", + "maintenance_start": "ನಿರ್ವಹಣಾ ಮೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ", + "maintenance_start_error": "ನಿರ್ವಹಣಾ ಕ್ರಮವನ್ನು ಪ್ರಾರಂಭಿಸಲು ವಿಫಲವಾಗಿದೆ.", + "manage_log_settings": "ಲಾಗ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ನಿರ್ವಹಿಸಿ", + "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_generate_job": "ಸ್ಮೃತಿ ಉತ್ಪಾದನೆ", + "metadata_extraction_job": "ಮೆಟಾಡೇಟಾವನ್ನು ಹೊರತೆಗೆಯಿರಿ", + "metadata_extraction_job_description": "GPS, ಮುಖಗಳು ಮತ್ತು ರೆಸಲ್ಯೂಶನ್‌ನಂತಹ ಪ್ರತಿ ಸ್ವತ್ತಿನಿಂದ ಮೆಟಾಡೇಟಾ ಮಾಹಿತಿಯನ್ನು ಹೊರತೆಗೆಯಿರಿ", + "metadata_faces_import_setting": "ಮುಖ ಆಮದು ಸಕ್ರಿಯಗೊಳಿಸಿ", + "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_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": "ಟೋನ್-ಮ್ಯಾಪಿಂಗ್" + }, + "administration": "ಆಡಳಿತ", + "advanced": "ಸುಧಾರಿತ", + "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..07c7c21891 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": "감지된 얼굴을 인물별로 그룹화합니다. 이 작업은 얼굴 감지 작업이 완료된 후 진행됩니다. \"초기화\"는 모든 얼굴을 다시 그룹화합니다. \"누락\"은 그룹화되지 않은 얼굴을 대기열에 추가합니다.", @@ -112,11 +120,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 +188,22 @@ "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_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": "지도 기능 활성화", @@ -270,6 +295,7 @@ "person_cleanup_job": "인물 정리", "queue_details": "대기열 상세", "queues": "작업 대기열", + "queues_page_description": "관리자 작업 대기열 페이지", "quota_size_gib": "할당량 (GiB)", "refreshing_all_libraries": "모든 라이브러리를 새로고침합니다.", "registration": "관리자 등록", @@ -287,8 +313,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": "개별 사진이 표시되는 초 단위의 시간", @@ -407,6 +435,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": "버전 확인", @@ -454,10 +484,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,6 +507,7 @@ "albums_default_sort_order_description": "새 앨범 생성 시 적용되는 기본 정렬을 설정합니다.", "albums_feature_description": "여러 사진과 동영상을 한곳에 모아 둘 수 있습니다.", "albums_on_device_count": "기기의 앨범 ({count}개)", + "albums_selected": "{count, plural, one {#개} other {#개}} 앨범 선택됨", "all": "모두", "all_albums": "모든 앨범", "all_people": "모든 인물", @@ -511,10 +544,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": "항목에 할당되지 않은 얼굴이 있음", @@ -693,10 +728,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 +746,11 @@ "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}개의 항목을 찾았습니다", "clear": "지우기", "clear_all": "모두 지우기", "clear_all_recent_searches": "검색 기록 전체 삭제", @@ -728,6 +771,7 @@ "collapse_all": "모두 접기", "color": "색상", "color_theme": "테마 색상", + "command": "명령", "comment_deleted": "댓글이 삭제되었습니다.", "comment_options": "댓글 옵션", "comments_and_likes": "댓글 및 좋아요", @@ -772,6 +816,7 @@ "create_album": "앨범 생성", "create_album_page_untitled": "제목 없음", "create_api_key": "API 키 생성", + "create_first_workflow": "첫 번째 워크플로를 생성합니다", "create_library": "새 라이브러리", "create_link": "링크 생성", "create_link_to_share": "공유 링크 생성", @@ -786,10 +831,13 @@ "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": "현재 PIN 코드", @@ -801,6 +849,7 @@ "daily_title_text_date_year": "yyyy년 M월 d일 EEEE", "dark": "다크", "dark_theme": "다크 테마 토글", + "date": "날짜", "date_after": "다음 날짜 이후", "date_and_time": "날짜 및 시간", "date_before": "다음 날짜 전", @@ -851,6 +900,7 @@ "deselect_all": "모두 선택 해제", "details": "상세 정보", "direction": "방향", + "disable": "비활성화", "disabled": "비활성화", "disallow_edits": "뷰어로 설정", "discord": "Discord", @@ -913,11 +963,15 @@ "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_flip_horizontal": "좌우반전", + "editor_flip_vertical": "상하반전", + "editor_rotate_left": "반시계 방향으로 90° 회전", + "editor_rotate_right": "시계 방향으로 90° 회전", "email": "이메일", "email_notifications": "이메일 알림", "empty_folder": "폴더가 비어 있음", @@ -998,6 +1052,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 +1063,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 +1114,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 +1161,16 @@ "features": "기능", "features_in_development": "개발 중인 기능", "features_setting_description": "사진 및 동영상 관리 기능을 설정합니다.", - "file_name": "파일 이름", + "file_name": "파일 이름: {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 +1180,14 @@ "folders_feature_description": "파일 시스템의 사진과 동영상을 폴더 보기로 탐색합니다.", "forgot_pin_code_question": "PIN 번호를 잊어버렸나요?", "forward": "앞으로", + "free_up_space_description": "백업된 사진과 동영상을 기기의 휴지통으로 이동하여 저장 공간을 확보하세요. 원본 파일은 서버에 안전하게 보관됩니다", "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 +1220,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 +1268,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,8 +1293,11 @@ "ios_debug_info_processing_ran_at": "{dateTime}에 처리됨", "items_count": "{count, plural, one {#개} other {#개}} 항목", "jobs": "작업", + "json_editor": "JSON 편집기", + "json_error": "JSON 오류", "keep": "유지", "keep_all": "모두 유지", + "keep_favorites": "즐겨찾기 유지", "keep_this_delete_others": "이 항목은 유지하고 나머지는 삭제", "kept_this_deleted_others": "이 항목을 유지하고 {count, plural, one {#개의 항목} other {#개의 항목}}을 삭제함", "keyboard_shortcuts": "키보드 단축키", @@ -1273,6 +1340,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 +1392,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": "앱 동작 및 표시 환경을 사용자 정의합니다.", @@ -1388,11 +1465,13 @@ "monthly_title_text_date_format": "yyyy년 M월", "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_up": "위로 이동", "moved_to_archive": "보관함으로 항목 {count, plural, one {#개} other {#개}} 이동됨", "moved_to_library": "라이브러리로 항목 {count, plural, one {#개} other {#개}} 이동됨", "moved_to_trash": "휴지통으로 이동되었습니다.", @@ -1402,6 +1481,7 @@ "my_albums": "내 앨범", "name": "이름", "name_or_nickname": "이름 또는 닉네임", + "name_required": "이름은 필수 입력 사항입니다", "navigate": "탐색", "navigate_to_time": "시간으로 탐색", "network_requirement_photos_upload": "사진 백업에 모바일 데이터 사용", @@ -1419,12 +1499,14 @@ "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_message": "앨범을 생성하여 사진과 동영상을 정리하기", "no_albums_with_name_yet": "아직 해당하는 이름의 앨범이 없는 것 같습니다.", "no_albums_yet": "아직 앨범이 없는 것 같습니다.", @@ -1434,12 +1516,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": "알림 없음", @@ -1467,6 +1553,7 @@ "oauth": "OAuth", "obtainium_configurator": "Obtainium 구성", "obtainium_configurator_instructions": "Obtainium으로 Immich GitHub 릴리스에서 직접 안드로이드 앱을 설치하고 업데이트하세요. API 키를 생성하고 변형을 선택해 Obtanium 설정 링크를 생성하세요.", + "ocr": "OCR", "official_immich_resources": "Immich 공식 리소스", "offline": "오프라인", "offset": "오프셋", @@ -1498,6 +1585,7 @@ "other_variables": "기타 변수", "owned": "소유함", "owner": "소유자", + "page": "페이지", "partner": "파트너", "partner_can_access": "{partner}님이 접근할 수 있는 항목", "partner_can_access_assets": "보관되거나 삭제된 항목을 제외한 모든 사진 및 동영상", @@ -1530,11 +1618,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 +1643,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": "사진 및 동영상", @@ -1747,6 +1838,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 +1892,9 @@ "second": "초", "see_all_people": "모든 인물 보기", "select": "선택", + "select_album": "앨범 선택", "select_album_cover": "앨범 커버 선택", + "select_albums": "앨범 선택", "select_all": "모두 선택", "select_all_duplicates": "비슷한 항목 모두 선택", "select_all_in": "{group}의 모든 항목 선택", @@ -1811,6 +1905,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 +1922,8 @@ "server_offline": "오프라인", "server_online": "온라인", "server_privacy": "개인정보", + "server_restarting_description": "이 페이지는 잠시 후 새로 고쳐집니다.", + "server_restarting_title": "서버가 재시작 중입니다", "server_stats": "서버 통계", "server_update_available": "서버 업데이트 가능", "server_version": "서버 버전", @@ -1944,11 +2042,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": "사이드바", @@ -2019,6 +2119,7 @@ "tags": "태그", "tap_to_run_job": "탭하여 작업 실행", "template": "템플릿", + "text_recognition": "텍스트 인식", "theme": "테마", "theme_selection": "테마 선택", "theme_selection_description": "시스템의 다크 모드 설정에 따라 테마를 자동으로 적용합니다.", @@ -2037,7 +2138,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 +2152,7 @@ "to_select": "선택", "to_trash": "삭제", "toggle_settings": "설정 변경", + "toggle_theme_description": "테마 전환", "total": "전체", "total_usage": "총 사용량", "trash": "휴지통", @@ -2066,6 +2170,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 코드를 변경할 수 없음", @@ -2096,13 +2207,14 @@ "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": "선택한 항목을 서버에 백업하시겠습니까?", @@ -2142,6 +2254,7 @@ "utilities": "도구", "validate": "검증", "validate_endpoint_error": "유효한 URL을 입력하세요.", + "validation_error": "유효성 검사 오류", "variables": "변수", "version": "버전", "version_announcement_closing": "당신의 친구, Alex가", @@ -2157,6 +2270,7 @@ "view_album": "앨범 보기", "view_all": "모두 보기", "view_all_users": "모든 사용자 보기", + "view_asset_owners": "자산 소유자 보기", "view_details": "상세 보기", "view_in_timeline": "타임라인에서 보기", "view_link": "링크 보기", @@ -2172,6 +2286,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 +2296,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..be386755e7 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,13 @@ "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_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į", @@ -31,8 +37,11 @@ "add_to_album_toggle": "Perjungti pažymėjimus albumui {album}", "add_to_albums": "Pridėti į albumus", "add_to_albums_count": "Pridėti į albumus ({count})", + "add_to_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 +74,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 +82,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.", @@ -99,6 +111,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 +119,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 +168,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 +187,12 @@ "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_settings": "Aptarnavimas", + "maintenance_settings_description": "Perjungti „Immich“ į aptarnavimo režimą.", + "maintenance_start": "Paleisti aptarnavimo režimą", + "maintenance_start_error": "Nepavyko paleisti aptarnavimo režimo.", "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 +242,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ą", @@ -246,10 +282,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 +302,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 +373,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 +391,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 +424,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", @@ -399,11 +443,11 @@ "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 +456,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,6 +473,7 @@ "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", @@ -449,6 +495,7 @@ "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", @@ -457,16 +504,21 @@ "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", "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 +532,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ų", @@ -550,6 +604,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 +612,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 +662,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 +672,7 @@ "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", "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 +714,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,6 +730,7 @@ "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", "clear": "Išvalyti", @@ -682,14 +745,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 +797,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 +813,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š", @@ -812,6 +887,7 @@ "deselect_all": "Atžymėti visus", "details": "Detalės", "direction": "Kryptis", + "disable": "Išjungti", "disabled": "Išjungta", "disallow_edits": "Neleisti redaguoti", "discord": "Discord", @@ -837,6 +913,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 +923,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", @@ -874,11 +952,17 @@ "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 +994,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 +1020,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 +1043,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 +1054,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 +1094,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 +1106,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,6 +1141,7 @@ "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ą", @@ -1062,7 +1154,7 @@ "features": "Funkcijos", "features_in_development": "Kūrimo funkcijos", "features_setting_description": "Valdyti aplikacijos funkcijas", - "file_name": "Failo pavadinimas", + "file_name": "Failo pavadinimas: {file_name}", "file_name_or_extension": "Failo pavadinimas arba plėtinys", "file_size": "Failo dydis", "filename": "Failopavadinimas", @@ -1070,6 +1162,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 +1172,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 įrenginio saugyklą", + "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 +1202,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 +1261,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,8 +1287,11 @@ "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_all": "Palikti visus", + "keep_favorites": "Palikti mėgstamiausius", "keep_this_delete_others": "Išsaugoti šį, kitus ištrinti", "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", @@ -1196,6 +1302,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 +1312,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 +1334,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 +1386,17 @@ "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_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 +1451,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 +1478,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 +1490,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,6 +1533,7 @@ "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ų", + "not_allowed": "Neleidžiama", "not_available": "Nepasiekiamas", "not_in_any_album": "Nė viename albume", "not_selected": "Nepasirinkta", @@ -1410,6 +1548,9 @@ "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", @@ -1478,7 +1619,7 @@ "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 {šitą elementą?} other {šituos # elementus?} other {šitų # elementų?}} Tuo pačiu {count, plural, one {jis bus pašalintas} other {jie bus pašalinti}} iš 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", @@ -1603,6 +1744,7 @@ "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_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", @@ -1624,6 +1766,7 @@ "reset_pin_code_description": "Jei pamiršote PIN kodą, galite susisiekti su serverio administratoriumi, kad jis jį atstatytų", "reset_pin_code_with_password": "PIN kodą visada galite atkurti naudodami savo slaptažodį", "reset_to_default": "Atkurti numatytuosius", + "resolution": "Rezoliucija", "resolve_duplicates": "Sutvarkyti dublikatus", "resolved_all_duplicates": "Sutvarkyti visi dublikatai", "restore": "Atkurti", @@ -1633,14 +1776,17 @@ "review_duplicates": "Peržiūrėti dublikatus", "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ų", @@ -1653,24 +1799,29 @@ "search_camera_model": "Ieškoti kameros modelį...", "search_city": "Ieškoti miesto...", "search_country": "Ieškoti šalies...", + "search_filter_camera_title": "Pasirinkti kameros tipą", "search_filter_date": "Data", "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_no_more_result": "Nėra daugiau rezultatų", "search_no_people_named": "Nėra žmonių vardu „{name}“", + "search_page_categories": "Kategorijos", "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_tags": "Ieškoti žymų...", "search_timezone": "Ieškoti laiko zonos...", @@ -1689,6 +1840,7 @@ "select_trash_all": "Visus pažymėti \"Išmesti\"", "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", @@ -1708,6 +1860,7 @@ "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_minutes": "{count} minučių", @@ -1797,37 +1950,49 @@ "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", "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_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", "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", @@ -1927,20 +2092,38 @@ "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_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..e89efb0f1a 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,20 @@ "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_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 +210,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 +263,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 +276,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 +373,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 +388,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 +400,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 +422,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 +452,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 +607,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", @@ -584,6 +637,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 +668,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 +679,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 +754,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 +764,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 +788,16 @@ "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_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 +811,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 +864,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 +891,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", @@ -833,12 +907,17 @@ "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 +945,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 +1006,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 +1034,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 +1091,22 @@ "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_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", @@ -1048,6 +1156,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 +1166,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", @@ -1087,6 +1201,7 @@ "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 +1211,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,6 +1221,7 @@ "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", @@ -1147,6 +1264,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 +1298,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 +1363,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 +1431,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 +1457,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,7 +1484,9 @@ "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_albums": "Izvēlies albumus", "select_all_duplicates": "Atlasīt visus dublikātus", "select_avatar_color": "Izvēlies avatāra krāsu", "select_face": "Izvēlies seju", @@ -1365,6 +1494,8 @@ "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 +1505,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 +1605,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 +1622,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", @@ -1520,6 +1657,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 +1672,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 +1682,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 +1700,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 +1712,19 @@ "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", "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 +1753,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 +1763,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 +1783,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..bb2df03977 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": "Овозможи", diff --git a/i18n/ml.json b/i18n/ml.json index 09877f7f53..86fbbe2812 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": "ഈ ഫോൾഡർ ശൂന്യമാണ്", @@ -2122,7 +2127,6 @@ "updated_at": "അപ്ഡേറ്റ് ചെയ്തത്", "updated_password": "പാസ്‌വേഡ് അപ്ഡേറ്റ് ചെയ്തു", "upload": "അപ്‌ലോഡ്", - "upload_action_prompt": "{count} എണ്ണം അപ്‌ലോഡിനായി ക്യൂവിൽ ചേർത്തു", "upload_concurrency": "അപ്‌ലോഡ് കോൺകറൻസി", "upload_details": "അപ്‌ലോഡ് വിശദാംശങ്ങൾ", "upload_dialog_info": "തിരഞ്ഞെടുത്ത അസറ്റ്(കൾ) സെർവറിലേക്ക് ബാക്കപ്പ് ചെയ്യണോ?", @@ -2198,7 +2202,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..1592a56ade 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": "हा फोल्डर रिकामा आहे", @@ -2122,7 +2122,6 @@ "updated_at": "अद्ययावत केले", "updated_password": "परवलीचा शब्द अद्ययावत केला", "upload": "अपलोड", - "upload_action_prompt": "अपलोडसाठी {count} रांगेत", "upload_concurrency": "अपलोड समांतरता", "upload_details": "अपलोड तपशील", "upload_dialog_info": "निवडलेले आयटम सर्व्हरवर बॅकअप करायचे का?", @@ -2198,7 +2197,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..77218cf29b 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", @@ -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", @@ -252,7 +272,7 @@ "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", @@ -363,7 +383,7 @@ "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", @@ -652,6 +684,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 +743,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 +753,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 +833,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 +848,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 +922,7 @@ "deselect_all": "Avmerk alle", "details": "Detaljer", "direction": "Retning", + "disable": "Deaktiver", "disabled": "Deaktivert", "disallow_edits": "Forby redigering", "discord": "Discord", @@ -890,6 +948,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 +958,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 +987,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", @@ -950,11 +1016,14 @@ "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 +1080,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 +1092,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 +1132,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 +1144,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", @@ -1118,14 +1192,16 @@ "features": "Funksjoner", "features_in_development": "Funksjoner under utvikling", "features_setting_description": "Administrerer funksjoner for appen", - "file_name": "Filnavn", + "file_name": "Filnavn: {file_name}", "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 +1211,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 +1246,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 +1326,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 +1379,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", @@ -1339,10 +1431,28 @@ "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", @@ -1404,6 +1514,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 +1524,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 +1541,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 +1566,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,6 +1597,7 @@ "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", @@ -1559,6 +1680,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 +1705,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", @@ -1663,10 +1788,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_set": "Vurdering satt til {rating, plural, one {# stjerne} other {# stjerner}}", "reaction_options": "Reaksjonsalternativer", "read_changelog": "Les endringslogg", "readonly_mode_disabled": "Skrivebeskyttet modus deaktivert", @@ -1766,9 +1893,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", @@ -1798,6 +1927,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", @@ -1832,17 +1962,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 +2114,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 +2132,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", @@ -2071,6 +2210,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 +2245,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 +2266,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,13 +2283,14 @@ "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?", @@ -2160,7 +2309,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.", @@ -2181,6 +2330,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 +2342,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 +2363,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..4bcd518f55 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­tijdigheid 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", @@ -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", @@ -575,7 +607,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 +623,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 +678,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 +743,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 +756,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,7 +777,7 @@ "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_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", @@ -787,6 +833,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 +848,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 {jaren}}", "daily_title_text_date": "E dd MMM", "daily_title_text_date_year": "E dd MMM yyyy", "dark": "Donker", @@ -867,6 +922,7 @@ "deselect_all": "Alles deselecteren", "details": "Details", "direction": "Richting", + "disable": "Uitschakelen", "disabled": "Uitgeschakeld", "disallow_edits": "Geen bewerkingen toestaan", "discord": "Discord", @@ -892,6 +948,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 +958,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 +987,17 @@ "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_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 +1016,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 +1081,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 +1092,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 +1132,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 +1144,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...", @@ -1120,14 +1192,16 @@ "features": "Functies", "features_in_development": "Functies in ontwikkeling", "features_setting_description": "Beheer de app functies", - "file_name": "Bestandsnaam", + "file_name": "Bestandsnaam: {file_name}", "file_name_or_extension": "Bestandsnaam of extensie", "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 +1211,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 +1253,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 +1326,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 +1431,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": "Tijdelijk niet beschikbaar", "make": "Merk", "manage_geolocation": "Beheer locatie", @@ -1408,6 +1514,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 +1524,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 +1541,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 +1549,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 +1566,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,6 +1597,7 @@ "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", @@ -1563,6 +1680,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 +1705,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 +1788,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", @@ -1739,7 +1862,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 +1893,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 +1927,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 +1962,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 +1988,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 +2114,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 +2132,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,7 +2167,7 @@ "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", @@ -2044,7 +2179,7 @@ "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 +2210,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 +2245,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 +2266,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,13 +2283,14 @@ "unstack": "Ontstapelen", "unstack_action_prompt": "{count} item(s) ontstapeld", "unstacked_assets_count": "{count, plural, one {# item} other {# items}} ontstapeld", + "unsupported_field_type": "Veldtype niet ondersteund", "untagged": "Ongemarkeerd", + "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?", @@ -2164,7 +2309,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 +2330,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 +2342,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 +2363,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 +2373,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/package.json b/i18n/package.json index 19d78c49b7..efb0458819 100644 --- a/i18n/package.json +++ b/i18n/package.json @@ -1,6 +1,6 @@ { "name": "immich-i18n", - "version": "1.0.0", + "version": "2.5.2", "private": true, "scripts": { "format": "prettier --check .", diff --git a/i18n/pl.json b/i18n/pl.json index 12828dca83..1d0f564c89 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", @@ -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", @@ -711,6 +743,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 +756,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", @@ -787,6 +833,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 +848,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": "Przechowuj 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 +922,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 +948,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 +958,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 +987,17 @@ "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_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 +1016,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 +1081,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 +1092,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 +1132,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 +1144,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 +1192,16 @@ "features": "Funkcje", "features_in_development": "Funkcje w fazie rozwoju", "features_setting_description": "Zarządzaj funkcjami aplikacji", - "file_name": "Nazwa pliku", + "file_name": "Nazwa pliku: {file_name}", "file_name_or_extension": "Nazwie lub rozszerzeniu pliku", "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 +1211,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 +1253,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 +1326,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": "Przechowano {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 , co zachować na tym urządzeniu", "keep_this_delete_others": "Zachowaj to, usuń pozostałe", + "keeping": "Przechowano:{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 +1431,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 +1514,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 +1524,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 +1541,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 +1566,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,6 +1597,7 @@ "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": "Pusto", "not_allowed": "Niedozwolone", "not_available": "Nie dotyczy", "not_in_any_album": "Bez albumu", @@ -1528,7 +1645,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 +1680,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 +1705,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 +1788,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", @@ -1770,9 +1893,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 +1927,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 +1962,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", @@ -1982,6 +2114,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 +2132,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 +2210,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 +2245,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 +2266,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,13 +2283,14 @@ "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?", @@ -2164,7 +2309,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 +2330,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 +2342,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 +2363,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 +2373,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..67b2471415 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}}", @@ -467,10 +474,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,6 +497,7 @@ "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", @@ -524,10 +534,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", @@ -711,6 +723,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 +736,17 @@ "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_icloud_shared_albums_excluded": "Álbuns Partilhados do iCloud serão excluídos da pesquisa", + "cleanup_no_assets_found": "Nenhum ficheiro de cópia de segurança encontrado que siga os seus critérios", + "cleanup_preview_title": "Ficheiros a serem removidos ({count})", + "cleanup_step3_description": "Procurar por fotos e vídeos que tenham sido copiados para o servidor com a data limite e as opções de filtro selecionadas", + "cleanup_step4_summary": "{count} ficheiros criados antes de {date} estão em espera para serem removidos do seu dispositivo", + "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", @@ -787,6 +812,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 +827,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": "Remover fotos e vídeos anteriores a", + "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 +901,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 +927,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 +937,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 +966,17 @@ "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_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", @@ -1014,6 +1057,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 +1068,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 +1108,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 +1120,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 +1168,16 @@ "features": "Funcionalidades", "features_in_development": "Funcionalidades em Desenvolvimento", "features_setting_description": "Configurar as funcionalidades da aplicação", - "file_name": "Nome do ficheiro", + "file_name": "Nome do ficheiro: {file_name}", "file_name_or_extension": "Nome do ficheiro ou extensão", "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 +1187,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 +1229,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,8 +1302,11 @@ "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_all": "Manter Todos", + "keep_favorites": "Manter favoritos", "keep_this_delete_others": "Manter este ficheiro, eliminar os outros", "kept_this_deleted_others": "Foi mantido ficheiro e {count, plural, one {eliminado # outro} other {eliminados # outros}}", "keyboard_shortcuts": "Atalhos do teclado", @@ -1408,6 +1466,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 +1476,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 +1493,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,6 +1518,7 @@ "next": "Avançar", "next_memory": "Próxima memória", "no": "Não", + "no_actions_added": "Ainda não foram adicionadas ações", "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.", @@ -1463,11 +1528,13 @@ "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", @@ -1563,6 +1630,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 +1655,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 +1738,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", @@ -1770,9 +1843,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", @@ -1836,17 +1911,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 +2063,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", @@ -2109,6 +2191,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", @@ -2139,13 +2228,14 @@ "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?", @@ -2185,6 +2275,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 +2287,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 +2308,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 +2318,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..452784d591 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.", @@ -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", @@ -587,11 +619,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 +743,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 +756,17 @@ "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_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", + "cleanup_preview_title": "Remover {count} arquivos", + "cleanup_step3_description": "Procurar por fotos e vídeos que já tem o backup feito no servidor até a data de corte e mais outros filtros selecionados", + "cleanup_step4_summary": "{count} arquivos criados antes de {date} foram selecionados para liberar espaço do seu dispositivo", + "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", @@ -787,6 +832,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 +847,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": "Remover fotos mais antigas que", + "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 +921,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 +947,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 +957,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 +986,17 @@ "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_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", @@ -1014,6 +1077,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 +1088,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 +1128,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 +1140,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 +1188,16 @@ "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": "Arquivo: {file_name}", "file_name_or_extension": "Nome do arquivo ou extensão", "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 +1207,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": "Libere espaço ao mover as fotos e vídeos já com backup no servidor para a lixeira do seu dispositivo. As cópias no servidor ainda existirão e estão a salvo", + "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 +1249,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,8 +1322,11 @@ "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_all": "Manter Todos", + "keep_favorites": "Manter favoritos", "keep_this_delete_others": "Manter este, excluir o resto", "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", @@ -1256,11 +1334,11 @@ "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", @@ -1408,6 +1486,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 +1496,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 +1513,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,6 +1538,7 @@ "next": "Avançar", "next_memory": "Próxima memória", "no": "Não", + "no_actions_added": "Nenhuma ação foi adicionada ainda", "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.", @@ -1463,11 +1548,13 @@ "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", @@ -1563,6 +1650,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 +1675,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 +1758,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", @@ -1770,9 +1863,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", @@ -1836,17 +1931,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", @@ -1982,6 +2083,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", @@ -2030,7 +2132,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 +2163,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.", @@ -2109,6 +2211,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", @@ -2139,13 +2248,14 @@ "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?", @@ -2164,7 +2274,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 +2295,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 +2307,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 +2328,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 +2338,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..c1b4046c67 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/**”.", @@ -110,9 +117,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 +188,18 @@ "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_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_settings": "Întreținere", "maintenance_settings_description": "Puneți Immich în modul de întreținere.", "maintenance_start": "Pornește 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.", + "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", @@ -277,10 +291,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", @@ -464,10 +480,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,6 +503,7 @@ "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ăra, plural, unul {# album selectat} altele {# albumuri selectate}}", "all": "Toate", "all_albums": "Toate albumele", "all_people": "Toți oamenii", @@ -521,10 +540,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", @@ -649,6 +670,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 +703,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 +729,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 +739,20 @@ "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_icloud_shared_albums_excluded": "Albumele partajate iCLoud sunt excluse de la cautare", + "cleanup_no_assets_found": "Nici un material in copia de rezerva găsit după criteriu", + "cleanup_preview_title": "Materiale sa fie șterse ({count})", + "cleanup_step3_description": "Scanați pentru fotografii și videoclipuri pentru care au fost făcute copii de rezervă pe server cu data limită selectată și opțiunile de filtrare", + "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", @@ -782,6 +818,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 +833,24 @@ "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": "Eliminați fotografiile și videoclipurile mai vechi de", + "cutoff_day": "{count, plural, o {day} mai multe {days}}", "daily_title_text_date": "E, LLL zz", "daily_title_text_date_year": "E, LLL zz, aaaa", "dark": "Întunecat", @@ -862,6 +906,7 @@ "deselect_all": "Deselectează toate", "details": "Detalii", "direction": "Direcție", + "disable": "Dezactivare", "disabled": "Dezactivat", "disallow_edits": "Interzice modificările", "discord": "Server Discord", @@ -887,6 +932,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 +942,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 +971,17 @@ "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_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", @@ -1009,6 +1062,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 +1073,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 +1113,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 +1125,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 +1160,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 +1173,16 @@ "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": "Nume de fișier: {file_name}", "file_name_or_extension": "Numele sau extensia fișierului", "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 +1192,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 +1227,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,8 +1307,11 @@ "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_all": "Păstrați Tot", + "keep_favorites": "Păstrați favoritele", "keep_this_delete_others": "Păstrați asta, ștergeți celelalte", "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ă", @@ -1284,6 +1354,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ă", @@ -1400,6 +1471,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 +1481,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 +1498,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,6 +1523,7 @@ "next": "Următorul", "next_memory": "Următoarea amintire", "no": "Nu", + "no_actions_added": "Nu s-au adăugat încă acțiuni", "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.", @@ -1455,11 +1533,13 @@ "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ă", @@ -1576,14 +1656,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", @@ -1660,9 +1743,10 @@ "query_asset_id": "Interoghează ID-ul resursei", "queue_status": "Se pun în coadă {count}/{total}", "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", @@ -1762,9 +1846,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", @@ -1826,7 +1912,7 @@ "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_cover": "Selectați coperta albumului", "select_all": "Selectați tot", @@ -1984,7 +2070,7 @@ "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", @@ -2137,7 +2223,6 @@ "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?", @@ -2188,19 +2273,19 @@ "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", + "view": "Secțiune", + "view_album": "Vizualizează Album", + "view_all": "Vizualizează Tot", "view_all_users": "Vizulizați toți utilizatorii", "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", @@ -2213,7 +2298,6 @@ "welcome": "Bun venit", "welcome_to_immich": "Bun venit la Immich", "wifi_name": "Nume Wi-Fi", - "workflow": "Flux de lucru", "wrong_pin_code": "Cod PIN greșit", "year": "An", "years_ago": "acum {years, plural, one {# an} other {# ani}} în urmă", diff --git a/i18n/ru.json b/i18n/ru.json index b7561b084c..01b5b3a2f4 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": "Регистрация администратора", @@ -296,10 +316,10 @@ "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": "Очистить все недавние результаты поиска", @@ -787,6 +836,7 @@ "create_album": "Создать альбом", "create_album_page_untitled": "Без названия", "create_api_key": "Создать API ключ", + "create_first_workflow": "Создать первый рабочий процесс", "create_library": "Создать библиотеку", "create_link": "Создать ссылку", "create_link_to_share": "Создать ссылку общего доступа", @@ -801,17 +851,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 +925,7 @@ "deselect_all": "Снять выделение", "details": "Подробности", "direction": "Направление", + "disable": "Отключить", "disabled": "Отключено", "disallow_edits": "Запретить редактирование", "discord": "Discord", @@ -892,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": "Управление настройками скачивания объектов", @@ -901,6 +961,7 @@ "download_waiting_to_retry": "Ожидание повторной попытки", "downloading": "Загрузка", "downloading_asset_filename": "Загрузка объекта {filename}", + "downloading_from_icloud": "Загрузка из iCloud", "downloading_media": "Загрузка медиа", "drop_files_to_upload": "Перенесите файлы в любое место для загрузки", "duplicates": "Дубликаты", @@ -929,11 +990,17 @@ "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_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 +1019,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 +1084,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 +1095,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 +1135,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 +1147,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 +1195,16 @@ "features": "Дополнительные возможности", "features_in_development": "Функции в разработке", "features_setting_description": "Управление дополнительными возможностями приложения", - "file_name": "Имя файла", + "file_name": "Имя файла: {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": "Исправить неправильное соответствие", @@ -1137,12 +1214,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 +1256,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 +1329,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 +1434,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 +1517,8 @@ "minimize": "Минимизировать", "minute": "Минута", "minutes": "Минуты", + "mirror_horizontal": "Горизонтально", + "mirror_vertical": "Вертикально", "missing": "Отсутствующие", "mobile_app": "Мобильное приложение", "mobile_app_download_onboarding_note": "Загрузите мобильное приложение Immich любым из следующих способов", @@ -1416,11 +1527,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 +1544,7 @@ "my_albums": "Мои альбомы", "name": "Имя", "name_or_nickname": "Имя или ник", + "name_required": "Имя обязательно для заполнения", "navigate": "Перейти", "navigate_to_time": "Перейти к дате", "network_requirement_photos_upload": "Использовать мобильный интернет для загрузки фото", @@ -1454,20 +1569,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,6 +1600,7 @@ "no_results_description": "Попробуйте использовать синонимы или более общие слова", "no_shared_albums_message": "Создавайте альбомы для обмена фотографиями и видеозаписями с людьми в вашей сети", "no_uploads_in_progress": "Нет активных загрузок", + "none": "Ничего", "not_allowed": "Запрещено", "not_available": "Нет данных", "not_in_any_album": "Ни в одном альбоме", @@ -1563,6 +1683,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 +1708,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 +1791,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": "Режим «только просмотр» отключён", @@ -1770,9 +1896,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 +1930,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 +1965,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 +2073,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 +2117,7 @@ "show_password": "Показать пароль", "show_person_options": "Действия с человеком", "show_progress_bar": "Отображать индикатор выполнения", + "show_schema": "Показать схему", "show_search_options": "Показать параметры поиска", "show_shared_links": "Показать публичные ссылки", "show_slideshow_transition": "Плавный переход", @@ -1999,6 +2135,8 @@ "skip_to_folders": "Перейти к папкам", "skip_to_tags": "Перейти к тегам", "slideshow": "Слайд-шоу", + "slideshow_repeat": "Зациклить слайд-шоу", + "slideshow_repeat_description": "Повторять слайд-шоу после его окончания", "slideshow_settings": "Настройки слайд-шоу", "sort_albums_by": "Сортировать альбомы по...", "sort_created": "Дата создания", @@ -2075,6 +2213,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 +2248,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 +2269,7 @@ "unhide_person": "Показать человека", "unknown": "Неизвестно", "unknown_country": "Неизвестная страна", + "unknown_date": "Дата неизвестна", "unknown_year": "Неизвестный Год", "unlimited": "Не ограничено", "unlink_motion_video": "Отсоединить движущееся видео", @@ -2139,17 +2286,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 +2334,7 @@ "utilities": "Утилиты", "validate": "Проверить", "validate_endpoint_error": "Введите корректный URL", + "validation_error": "Ошибка при проверке", "variables": "Переменные", "version": "Версия", "version_announcement_closing": "Твой друг Алекс", @@ -2196,6 +2346,7 @@ "video_hover_setting_description": "Воспроизводить видео при наведении курсора мыши на миниатюру. Даже если эта функция выключена, воспроизведение можно запустить, наведя курсор на значок воспроизведения.", "videos": "Видео", "videos_count": "{count, plural, one {# видео} other {# видео}}", + "videos_only": "Только видео", "view": "Просмотр", "view_album": "Открыть альбом", "view_all": "Посмотреть всё", @@ -2216,21 +2367,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..f628995254 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", @@ -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", @@ -646,13 +678,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 +743,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 +756,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", @@ -787,6 +833,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 +842,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…", + "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 +922,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 +948,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 +958,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 +987,17 @@ "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_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 +1016,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 +1081,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 +1092,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 +1130,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 +1144,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...", @@ -1116,18 +1188,20 @@ "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": "Názov súboru: {file_name}", "file_name_or_extension": "Názov alebo prípona súboru", "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 +1211,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 +1253,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 +1326,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 +1431,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 +1514,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 +1524,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 +1541,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 +1566,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,6 +1597,7 @@ "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", @@ -1563,6 +1680,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 +1705,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 +1788,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ý", @@ -1770,9 +1893,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 +1907,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 +1927,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 +1962,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ť dátum konca", "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 +2070,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 +2114,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 +2132,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,7 +2167,7 @@ "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", @@ -2075,6 +2210,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 +2245,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 +2266,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,13 +2283,14 @@ "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?", @@ -2164,7 +2309,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 +2330,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 +2342,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 +2363,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 +2373,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..c9c52d3dcd 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", @@ -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", @@ -711,6 +743,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 +756,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", @@ -787,6 +833,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 +848,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 +922,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 +948,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 +958,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 +987,17 @@ "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_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 +1016,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 +1081,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 +1092,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 +1132,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 +1144,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..", @@ -1120,14 +1192,16 @@ "features": "Funkcije", "features_in_development": "Funkcije v razvoju", "features_setting_description": "Upravljaj funkcije aplikacije", - "file_name": "Ime datoteke", + "file_name": "Ime datoteke: {file_name}", "file_name_or_extension": "Ime ali končnica datoteke", "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 +1211,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", @@ -1175,6 +1253,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 +1326,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 +1431,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 +1514,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 +1524,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 +1541,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 +1566,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,6 +1597,7 @@ "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", @@ -1563,6 +1680,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", @@ -1587,11 +1705,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 +1788,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", @@ -1770,9 +1893,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 +1927,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 +1962,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 +2114,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 +2132,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", @@ -2075,6 +2210,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 +2245,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 +2266,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,13 +2283,14 @@ "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?", @@ -2164,7 +2309,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 +2330,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 +2342,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 +2363,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 +2373,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..13925c212d 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,13 @@ "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_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 +33,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 +60,56 @@ "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": "Detyrat 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 detyrë", + "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 detyrë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_title": "Cilësimet e parapamjes" + }, + "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..3888896ffb 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": "Ова мапа је празна", diff --git a/i18n/sr_Latn.json b/i18n/sr_Latn.json index ad490d491f..5b94e1361d 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 ć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 koristeći cron format. Za više informacija pogledajte npr. Crontab Guru", @@ -72,6 +82,7 @@ "disable_login": "Onemoguć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 omoguć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 pomoć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 će biti stavljena u red za prepoznavanje lica nakon što se prepoznavanje lica završi, grupišući ih u postojeće ili nove osobe.", "facial_recognition_job_description": "Grupa je detektovala lica i dodala ih postojećim osobama. Ovaj korak se pokreć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 već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": "Veće rezolucije mogu da sačuvaju više detalja, ali im je potrebno više vremena za kodiranje, imaju veće veličine datoteka i mogu da smanje odziv aplikacije.", @@ -795,8 +807,6 @@ "editor": "Urednik", "editor_close_without_save_prompt": "Promene neć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", diff --git a/i18n/sv.json b/i18n/sv.json index b8d33cd838..eb164be614 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", @@ -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", @@ -591,7 +623,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 +743,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 +756,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", @@ -787,6 +833,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 +848,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 +922,7 @@ "deselect_all": "Avmarkera alla", "details": "Detaljer", "direction": "Riktning", + "disable": "inaktivera", "disabled": "Inaktiverad", "disallow_edits": "Tillåt inte redigeringar", "discord": "Discord", @@ -892,6 +948,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 +958,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 +987,17 @@ "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_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 +1016,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 +1081,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 +1092,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 +1132,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 +1144,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 +1172,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 +1192,16 @@ "features": "Funktioner", "features_in_development": "Funktioner i utveckling", "features_setting_description": "Hantera appens funktioner", - "file_name": "Filnamn", + "file_name": "Filnamn: {file_name}", "file_name_or_extension": "Filnamn eller -tillägg", "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 +1211,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 +1253,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 +1326,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 +1431,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 +1514,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 +1524,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 +1541,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 +1566,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,6 +1597,7 @@ "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", @@ -1563,6 +1680,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 +1705,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 +1788,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", @@ -1770,9 +1893,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 +1906,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 +1927,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 +1962,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 +2114,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 +2132,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 +2169,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 +2210,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 +2245,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 +2266,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,13 +2283,14 @@ "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?", @@ -2185,6 +2330,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 +2342,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 +2363,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 +2373,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..1c8bb42b9f 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,13 @@ "add_a_location": "இடத்தை சேர்க்கவும்", "add_a_name": "பெயரை சேர்க்கவும்", "add_a_title": "தலைப்பு சேர்க்கவும்", + "add_action": "செயலைச் சேர்", + "add_action_description": "செய்ய வேண்டிய செயலைச் சேர்க்க கிளிக் செய்யவும்", "add_birthday": "பிறந்தநாளைச் சேர்க்கவும்", "add_endpoint": "சேவை நிரலை சேர்", "add_exclusion_pattern": "விலக்கு வடிவத்தைச் சேர்க்கவும்", + "add_filter": "வடிகட்டியைச் சேர்க்கவும்", + "add_filter_description": "வடிகட்டி நிபந்தனையைச் சேர்க்க கிளிக் செய்யவும்", "add_location": "இடத்தைச் சேர்க்கவும்", "add_more_users": "மேலும் பயனர்களை சேர்க்கவும்", "add_partner": "துணையை சேர்க்கவும்", @@ -35,6 +41,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} சேர்க்கப்பட்டது", @@ -112,6 +119,7 @@ "job_settings_description": "வேலை ஒத்திசைவை நிர்வகிக்கவும்", "jobs_delayed": "{jobCount, plural, other {# தாமதமானது}}", "jobs_failed": "{jobCount, plural, other {# தோல்வியுற்றது}}", + "jobs_over_time": "காலப்போக்கில் வேலைகள்", "library_created": "உருவாக்கப்பட்ட நூலகம்: {library}", "library_deleted": "புகைப்பட நூலகம் நீக்கப்பட்டது", "library_details": "நூலக விவரங்கள்", @@ -274,6 +282,7 @@ "password_settings_description": "கடவுச்சொல் உள்நுழைவு அமைப்புகளை நிர்வகிக்கவும்", "paths_validated_successfully": "அனைத்து பாதைகளும் வெற்றிகரமாக சரிபார்க்கப்பட்டன", "person_cleanup_job": "நபர் தூய்மைப்படுத்துதல்", + "queue_details": "வரிசை விவரங்கள்", "quota_size_gib": "ஒதுக்கீடு அளவு (GiB)", "refreshing_all_libraries": "அனைத்து நூலகங்களையும் புதுப்பிக்கிறது", "registration": "நிர்வாக பதிவு", @@ -924,8 +933,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": "இந்த கோப்புறை காலியாக உள்ளது", @@ -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..97c495987d 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మీరు ఈ చర్యను రద్దు చేయలేరు!", diff --git a/i18n/th.json b/i18n/th.json index c960fd8cb9..b887c68b7f 100644 --- a/i18n/th.json +++ b/i18n/th.json @@ -7,16 +7,19 @@ "action_common_update": "อัปเดต", "actions": "การดำเนินการ", "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_birthday": "เพิ่มวันเกิด", "add_endpoint": "เพิ่มปลายทาง", "add_exclusion_pattern": "เพิ่มข้อยกเว้น", + "add_filter": "เพิ่มตัวกรอง", "add_location": "เพิ่มตำแหน่ง", "add_more_users": "เพิ่มผู้ใช้งาน", "add_partner": "เพิ่มคู่หู", @@ -24,18 +27,19 @@ "add_photos": "เพิ่มรูปภาพ", "add_tag": "เพิ่มแท็ก", "add_to": "เพิ่มไปยัง …", - "add_to_album": "เพิ่มไปอัลบั้ม", - "add_to_album_bottom_sheet_added": "เพิ่มไปยัง {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_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": "ผู้ดูแล", @@ -72,6 +76,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": "นำใบหน้าที่ตรวจจับได้ไปจับกลุ่มตามผู้คน ขั้นตอนนี้ทำงานหลังจากตรวจจับใบหน้าสำเร็จ \"ทั้งหมด\" จะจำกลุ่มใบหน้าทั้งหมดใหม่ \"ขาดหาย\" จะจัดคิวใบหน้าที่ยังไม่ได้ระบุคน", @@ -827,8 +832,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": "โฟลเดอร์นี้ว่างเปล่า", @@ -1778,15 +1781,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 +1802,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 +1817,7 @@ "upload_status_uploaded": "อัปโหลดแล้ว", "upload_success": "อัปโหลดสำเร็จ, รีเฟรชหน้านี้ใหม่คุณจะเห็นสื่อที่เพิ่มล่าสุด", "uploading": "กำลังอัพโหลด", - "uploading_media": "อัปโหลดสื่อ", + "uploading_media": "กำลังอัปโหลดสื่อ", "usage": "การใช้งาน", "use_biometric": "ใช้การพิสูจน์อัตลักษณ์", "use_current_connection": "ใช้การเชื่อมต่อปัจจุบัน", @@ -1818,6 +1827,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 +1839,7 @@ "utilities": "เครื่องมือ", "validate": "ตรวจสอบ", "validate_endpoint_error": "กรุณาระบุ URL ที่ถูกต้อง", + "validation_error": "การตรวจสอบข้อมูลล้มเหลว", "variables": "ตัวแปร", "version": "รุ่น", "version_announcement_closing": "เพื่อนของคุณ อเล็กซ์", @@ -1839,6 +1850,7 @@ "video_hover_setting": "เล่นวิดีโอแบบย่อเมื่อเลื่อนเมาส์อยู่บน", "video_hover_setting_description": "เล่นวิดีโอตัวอย่างเมื่อเมาส์จ่อข้างบน เมื่อปิดใช้งาน วิดีโอตัวอย่างยังสามารถเล่นได้โดยกดปุ่มเล่น", "videos": "วิดีโอ", + "videos_only": "วิดีโอเท่านั้น", "view": "ดู", "view_album": "ดูอัลบั้ม", "view_all": "ดูทั้งหมด", @@ -1850,6 +1862,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 +1873,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..a77e23d0a1 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.", @@ -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", @@ -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", @@ -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", @@ -588,10 +620,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 Senkronizasyonunu Yedekle", "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 +743,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 +756,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 ölçütlere uyan hiçbir öğe bulunamadı. Alan Aç yalnızca sunucuya yedeklenmiş öğeleri 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", @@ -787,6 +833,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 +848,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 +922,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 +948,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 +958,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 +987,17 @@ "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_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 +1016,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 +1081,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 +1092,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 +1132,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,8 +1144,10 @@ "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_bottom_sheet_description": "Açıklama Ekle...", @@ -1120,14 +1192,16 @@ "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": "Dosya adı: {file_name}", "file_name_or_extension": "Dosya adı veya uzantı", "file_size": "Dosya boyutu", "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 +1211,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 +1253,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.", @@ -1247,9 +1326,18 @@ "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 +1431,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 +1514,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 +1524,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 +1541,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 +1566,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_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,6 +1597,7 @@ "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", @@ -1563,6 +1680,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 +1705,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 +1788,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ışı", @@ -1770,9 +1893,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 +1927,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 +1962,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", @@ -1982,6 +2114,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 +2132,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", @@ -2075,6 +2210,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 +2245,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 +2266,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,13 +2283,14 @@ "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?", @@ -2164,7 +2309,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 +2330,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 +2342,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 +2363,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 +2373,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..668871e902 100644 --- a/i18n/uk.json +++ b/i18n/uk.json @@ -5,19 +5,25 @@ "acknowledge": "Прийняти", "action": "Дія", "action_common_update": "Оновити", + "action_description": "Набір дій, які потрібно виконати з відфільтрованими фото та відео", "actions": "Дії", "active": "Виконується", - "active_count": "Активний: {count}", + "active_count": "Активні: {count}", "activity": "Активність", "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": "Додати адресу серверу", "add_exclusion_pattern": "Додати шаблон виключення", + "add_filter": "Додати фільтр", + "add_filter_description": "Натисніть, щоб додати умову фільтра", "add_location": "Додати місцезнаходження", "add_more_users": "Додати користувачів", "add_partner": "Додати партнера", @@ -28,7 +34,7 @@ "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})", @@ -36,13 +42,14 @@ "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, і проскануйте бібліотеку.", + "asset_offline_description": "Цей файл зовнішньої бібліотеки не знайдено на диску і був переміщений до смітника. Якщо файл був переміщений у межах бібліотеки, перевірте свою стрічку на наявність нового відповідного файлу. Щоб відновити цей файл, переконайтеся, що шлях до файлу доступний для Immich, і проскануйте бібліотеку.", "authentication_settings": "Налаштування аутентифікації", "authentication_settings_description": "Управління паролями, OAuth та іншими налаштуваннями аутентифікації", "authentication_settings_disable_all": "Ви впевнені, що хочете вимкнути всі методи входу? Вхід буде повністю вимкнений.", @@ -52,8 +59,8 @@ "backup_database_enable_description": "Увімкнути дампи бази даних", "backup_keep_last_amount": "Кількість попередніх дампів, які зберігати", "backup_onboarding_1_description": "віддалена копія у хмарі або в іншому фізичному місці.", - "backup_onboarding_2_description": "локальні копії на різних пристроях. Це включає основні файли і резервну копію цих файлів локально.", - "backup_onboarding_3_description": "загальні копії ваших даних, включаючи оригінальні файли. Це включає 1 віддалену копію і 2 локальні копії.", + "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 включає:", @@ -63,40 +70,42 @@ "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": "Вища роздільність може зберігати більше деталей, але займає більше часу для кодування, має більші розміри файлів і може зменшити швидкість роботи програми.", @@ -113,7 +122,7 @@ "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": "Якщо вимкнено, всі функції машинного навчання будуть вимкнені незалежно від налаштувань нижче.", @@ -180,18 +189,29 @@ "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_gps_settings": "Налаштування карти та геолокації", + "map_gps_settings_description": "Керування налаштуваннями карти та геолокації (зворотний геокодинг)", "map_implications": "Функція карти використовує зовнішній сервіс плиток (tiles.immich.cloud)", "map_light_style": "Світлий стиль", "map_manage_reverse_geocoding_settings": "Керувати налаштуваннями зворотного геокодування", @@ -201,16 +221,16 @@ "map_settings": "Мапа", "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": "Адреса відправника", "notification_email_from_address_description": "Адреса електронної пошти відправника, наприклад: \"Immich Photo Server \". Переконайтеся, що використовуєте адресу, з якої вам дозволено надсилати листи.", - "notification_email_host_description": "Хост поштового сервера (наприклад, smtp.immich.app)", + "notification_email_host_description": "Адреса поштового сервера (наприклад, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ігнорувати помилки сертифіката", "notification_email_ignore_certificate_errors_description": "Ігнорувати помилки перевірки сертифікатів TLS (не рекомендується)", "notification_email_password_description": "Пароль для аутентифікації на поштовому сервері", @@ -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 (ключ підтвердження для обміну кодом) не підтримується для публічного клієнта.", "oauth_enable_description": "Увійти за допомогою OAuth", "oauth_mobile_redirect_uri": "URI мобільного перенаправлення", "oauth_mobile_redirect_uri_override": "Перевизначення URI мобільного перенаправлення", @@ -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,39 +323,39 @@ "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": "Якщо шаблон порожній, буде використано стандартний ел. лист.", + "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_nvenc": "NVENC (вимагає графічного процесора NVIDIA)", @@ -357,9 +377,9 @@ "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 відео завжди буде транскодуватись (крім випадків, коли транскодування вимкнено).", @@ -396,25 +416,25 @@ "transcoding_two_pass_encoding_setting_description": "Транскодування за двома проходами для отримання кращих закодованих відео. Коли ввімкнено максимальний бітрейт (необхідний для роботи з H.264 та HEVC), цей режим використовує діапазон бітрейту, заснований на максимальному бітрейті, і ігнорує CRF. Для VP9 можна використовувати CRF, якщо вимкнено максимальний бітрейт.", "transcoding_video_codec": "Відеокодек", "transcoding_video_codec_description": "VP9 має високу ефективність і сумісність з вебом, але потребує більше часу на транскодування. HEVC працює схоже, але має меншу сумісність з вебом. H.264 має широку сумісність і швидко транскодується, але створює значно більші файли. AV1 - найефективніший кодек, але не підтримується на старіших пристроях.", - "trash_enabled_description": "Увімкнення кошика", + "trash_enabled_description": "Увімкнення смітника", "trash_number_of_days": "Кількість днів", - "trash_number_of_days_description": "Кількість днів, протягом якої залишати ресурси в кошику перед їх остаточним видаленням", - "trash_settings": "Налаштування кошика", - "trash_settings_description": "Керування налаштуваннями кошика", + "trash_number_of_days_description": "Кількість днів, протягом яких залишати файли у смітнику перед їх остаточним видаленням", + "trash_settings": "Налаштування смітника", + "trash_settings_description": "Керування налаштуваннями смітника", "unlink_all_oauth_accounts": "Від’єднати всі облікові записи OAuth", "unlink_all_oauth_accounts_description": "Не забудьте від’єднати всі облікові записи OAuth перед переходом до нового постачальника.", "unlink_all_oauth_accounts_prompt": "Ви впевнені, що хочете від’єднати всі облікові записи OAuth? Це скине ідентифікатор OAuth для кожного користувача, і цю дію не можна буде скасувати.", "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": "Керування налаштуваннями користувачів", @@ -431,10 +451,13 @@ "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": "Увімкніть додаткові функції для усунення несправностей", @@ -467,16 +490,18 @@ "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": "Поділитися", @@ -485,12 +510,14 @@ "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 {# вибрано альбом} other {# вибрані альбоми}}", "all": "Усі", "all_albums": "Усі альбоми", "all_people": "Усі люди", + "all_photos": "Усі фотографії", "all_videos": "Усі відео", "allow_dark_mode": "Дозволити темний режим", "allow_edits": "Дозволити редагування", @@ -498,15 +525,18 @@ "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_bar_signout_dialog_title": "Вийти", "app_download_links": "Посилання для завантаження додатків", "app_settings": "Налаштування програми", "app_stores": "Магазини додатків", @@ -514,9 +544,9 @@ "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_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 {# ресурс} 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": "файли", + "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, plural, one {# файл} few {# файли} other {# файлів}}", + "assets_deleted_permanently_from_server": "Видалено назавжди {count, plural, one {# файл} few {# файли} other {# файлів}} з сервера 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_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 {Файл вже був} other {Файли вже були}} частиною альбомів", "authorized_devices": "Авторизовані пристрої", "automatic_endpoint_switching_subtitle": "Підключатися локально через зазначену Wi-Fi мережу, коли це можливо, і використовувати альтернативні з'єднання в інших випадках", "automatic_endpoint_switching_title": "Автоматичне перемикання URL", @@ -587,32 +622,32 @@ "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": "Для фонового резервного копіювання увімкніть фонове оновлення в меню \"Налаштування > Загальні > Фонове оновлення програми\".", "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_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}]", @@ -643,7 +678,7 @@ "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": "Успіх", @@ -653,7 +688,7 @@ "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_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,30 +740,44 @@ "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": "Клієнтський сертифікат імпортовано", @@ -745,15 +794,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 +811,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": "Видалити з пристрою", @@ -770,8 +819,8 @@ "control_bottom_app_bar_edit_time": "Редагувати дату та час", "control_bottom_app_bar_share_link": "Поділитися", "control_bottom_app_bar_share_to": "Поділитися", - "control_bottom_app_bar_trash_from_immich": "До кошика", - "copied_image_to_clipboard": "Копіюємо зображення в буфер обміну.", + "control_bottom_app_bar_trash_from_immich": "До смітника", + "copied_image_to_clipboard": "Зображення скопійовано в буфер обміну.", "copied_to_clipboard": "Скопійовано в буфер обміну!", "copy_error": "Помилка копіювання", "copy_file_path": "Скопіювати шлях до файлу", @@ -787,31 +836,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 {день} other {дні}}", + "cutoff_year": "{count, plural, one {рік} other {роки}}", "daily_title_text_date": "Е, МММ дд", "daily_title_text_date_year": "Е, МММ дд, рррр", "dark": "Темна", @@ -833,14 +891,14 @@ "default_locale": "Дата і час за замовчуванням", "default_locale_description": "Форматувати дати та числа з урахуванням мови вашого браузера", "delete": "Видалити", - "delete_action_confirmation_message": "Ви впевнені, що хочете видалити цей файл? Його буде переміщено до кошика на сервері, а також зʼявиться запит на його видалення з пристрою", - "delete_action_prompt": "{count} видалено", + "delete_action_confirmation_message": "Ви впевнені, що хочете видалити цей файл? Його буде переміщено до смітника на сервері, а також зʼявиться запит на його видалення з пристрою", + "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,25 +906,26 @@ "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": "Видаляє ресурси, які відсутні на диску", + "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'", @@ -877,11 +936,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 +949,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": "Перенесіть файли в будь-яке місце для завантаження", "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 +990,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_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": "Очистити смітник", + "empty_trash_confirmation": "Ви впевнені, що хочете очистити смітник? Це остаточно видалить всі файли у смітнику з Immich.\nЦю дію не можна скасувати!", "enable": "Увімкнути", "enable_backup": "Увімкнути резервне копіювання", "enable_biometric_auth_description": "Введіть свій PIN-код, щоб увімкнути біометричну автентифікацію", @@ -950,45 +1017,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_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,36 +1067,38 @@ "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": "Неможливо очистити кошик", + "unable_to_empty_trash": "Неможливо очистити смітник", "unable_to_enter_fullscreen": "Неможливо увійти в повноекранний режим", "unable_to_exit_fullscreen": "Неможливо вийти з повноекранного режиму", "unable_to_get_comments_number": "Не вдалося отримати кількість коментарів", @@ -1038,19 +1110,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 +1135,9 @@ "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_trash_asset": "Неможливо видалити файл", "unable_to_unlink_account": "Не вдається відв'язати обліковий запис", "unable_to_unlink_motion_video": "Не вдається від'єднати рухоме відео", "unable_to_update_album_cover": "Неможливо оновити обкладинку альбому", @@ -1074,13 +1147,15 @@ "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": "Додати опис...", "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 +1184,53 @@ "failed": "Не вдалося", "failed_count": "Не вдалося: {count}", "failed_to_authenticate": "Помилка автентифікації", - "failed_to_load_assets": "Не вдалося завантажити ресурси", + "failed_to_load_assets": "Не вдалося завантажити файли", "failed_to_load_folder": "Не вдалося завантажити папку", "favorite": "До улюблених", "favorite_action_prompt": "{count} додано до обраного", "favorite_or_unfavorite_photo": "Додати до обраних або видалити з обраних фото", - "favorites": "Улюблені", - "favorites_page_no_favorites": "Немає улюблених елементів", + "favorites": "Обране", + "favorites_page_no_favorites": "Немає улюблених фото та відео", "feature_photo_updated": "Вибране фото оновлено", "features": "Додаткові можливості", "features_in_development": "Функції в розробці", "features_setting_description": "Керування додатковими можливостями застосунку", - "file_name": "Ім'я файлу", + "file_name": "Ім'я файлу: {file_name}", "file_name_or_extension": "Ім'я файлу або розширення", "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 +1241,7 @@ "haptic_feedback_switch": "Увімкнути тактильну віддачу", "haptic_feedback_title": "Тактильна віддача", "has_quota": "Квота", - "hash_asset": "Гешувати файл", + "hash_asset": "Хешувати файл", "hashed_assets": "Хеши", "hashing": "Хешування", "header_settings_add_header_tip": "Додати заголовок", @@ -1175,24 +1256,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,7 +1295,7 @@ "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", @@ -1225,7 +1307,7 @@ "in_year_selector": "У", "include_archived": "Відображати архів", "include_shared_albums": "Включити спільні альбоми", - "include_shared_partner_assets": "Включайте спільні партнерські ресурси", + "include_shared_partner_assets": "Включайте спільні партнерські файли", "individual_share": "Індивідуальний доступ", "individual_shares": "Окремі спільні доступи", "info": "Інформація", @@ -1245,12 +1327,21 @@ "ios_debug_info_no_sync_yet": "Фонове завдання синхронізації ще не запускалося", "ios_debug_info_processes_queued": "{count, plural, one {{count} фоновий процес у черзі} other {{count} фонових процесів у черзі}}", "ios_debug_info_processing_ran_at": "Обробку виконано {dateTime}", - "items_count": "{count, plural, one {# елемент} 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 {альбом} 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": "Спробуйте змінити пошуковий запит", @@ -1274,25 +1365,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": "Розташування", @@ -1316,11 +1407,11 @@ "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, перевірте адресу сервера", @@ -1338,39 +1429,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_title": "Доступ до керування медіа", "manage_shared_links": "Керування спільними посиланнями", - "manage_sharing_with_partners": "Керуйте спільним використанням з партнерами", + "manage_sharing_with_partners": "Керування спільним доступом з партнерами", "manage_the_app_settings": "Керування налаштуваннями програми", - "manage_your_account": "Керуйте своїм обліковим записом", + "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_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": "Темний режим", @@ -1388,26 +1497,28 @@ "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": "Завантажте супутній мобільний додаток, скориставшись наведеними нижче опціями", @@ -1416,20 +1527,24 @@ "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 {# файл} other {# файлів}} в архів", + "moved_to_library": "Переміщено {count, plural, one {# файл} other {# файлів}} в бібліотеку", + "moved_to_trash": "Переміщено до смітника", + "multiselect_grid_edit_date_time_err_read_only": "Неможливо редагувати дату файлів лише для читання, пропускаю", + "multiselect_grid_edit_gps_err_read_only": "Неможливо редагувати геолокацію файлів лише для читання, пропускаю", "mute_memories": "Приглушити спогади", "my_albums": "Мої альбоми", "name": "Ім'я", "name_or_nickname": "Ім'я або псевдонім", + "name_required": "Ім'я обов'язкове", "navigate": "Навігація", "navigate_to_time": "Перейти до Часу", "network_requirement_photos_upload": "Використовувати стільникові дані для резервного копіювання фото", @@ -1454,38 +1569,43 @@ "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": "Немає активних завантажень", + "none": "Жоден", "not_allowed": "Не дозволено", "not_available": "Немає даних", "not_in_any_album": "У жодному альбомі", "not_selected": "Не вибрано", - "note_apply_storage_label_to_previously_uploaded assets": "Примітка: Щоб застосувати мітку сховища до раніше завантажених ресурсів, виконайте команду", + "note_apply_storage_label_to_previously_uploaded assets": "Примітка: Щоб застосувати мітку сховища до раніше завантажених файлів, виконайте команду", "notes": "Нотатки", "nothing_here_yet": "Тут ще нічого немає", "notification_permission_dialog_content": "Щоб увімкнути сповіщення, перейдіть до Налаштувань і надайте дозвіл.", @@ -1500,16 +1620,16 @@ "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": "Доступний", @@ -1526,7 +1646,7 @@ "original": "оригінал", "other": "Інше", "other_devices": "Інші пристрої", - "other_entities": "Інші об'єкти", + "other_entities": "Інші файли", "other_variables": "Інші змінні", "owned": "Власні", "owner": "Власник", @@ -1546,16 +1666,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 +1683,17 @@ "people": "Люди", "people_edits_count": "Відредаговано {count, plural, one {# особу} few {# особи} many {# осіб} other {# людей}}", "people_feature_description": "Перегляд фотографій і відео, згрупованих за людьми", + "people_selected": "{count, plural, one {# обрана особа} other {# вибрані люди}}", "people_sidebar_description": "Відображення посилання на людей у бічній панелі", "permanent_deletion_warning": "Попередження про видалення", - "permanent_deletion_warning_setting_description": "Показувати попередження при остаточному видаленні ресурсів", + "permanent_deletion_warning_setting_description": "Показувати попередження при остаточному видаленні файлів", "permanently_delete": "Видалити назавжди", - "permanently_delete_assets_count": "Остаточно видалити {count, plural, one {ресурс} other {ресурси}}", - "permanently_delete_assets_prompt": "Ви впевнені, що хочете назавжди видалити {count, plural, one {цей ресурс?} other {ці # ресурси?}} Це також видалить {count, plural, one {його з його} other {їх з їхніх}} альбому(ів).", + "permanently_delete_assets_count": "Остаточно видалити {count, plural, one {файл} other {файли}}", + "permanently_delete_assets_prompt": "Ви впевнені, що хочете назавжди видалити {count, plural, one {цей файл?} other {ці # файли?}} Це також видалить {count, plural, one {його з його} other {їх з їхніх}} альбому(ів).", "permanently_deleted_asset": "Файл видалено назавжди", - "permanently_deleted_assets_count": "Видалено остаточно {count, plural, one {# ресурс} 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": "Розпочати", @@ -1583,15 +1704,18 @@ "permission_onboarding_request": "Застосунку Immich потрібен дозвіл для перегляду ваших фото та відео.", "person": "Людина", "person_age_months": "{months, plural, one {# місяць} other {# місяці}}", - "person_age_year_months": "1 year , {months, plural, one {# місяць} other {# місяці}}", + "person_age_year_months": "1 рік, {months, plural, one {# місяць} other {# місяці}}", "person_age_years": "{years, plural, other {# років}}", "person_birthdate": "Народився {date}", "person_hidden": "{name}{hidden, select, true { (приховано)} other {}}", + "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": "Виберіть діапазон дат", @@ -1615,7 +1739,7 @@ "preferences_settings_title": "Параметри", "preparing": "Підготовка", "preset": "Передвстановлення", - "preview": "Прев'ю", + "preview": "Попередній перегляд", "previous": "Попереднє", "previous_memory": "Попередній спогад", "previous_or_next_day": "День вперед/назад", @@ -1652,7 +1776,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,21 +1789,23 @@ "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 на інформаційній панелі", + "rating_set": "Рейтинг встановлено на {rating, plural, one {# зірка} other {# зірки}}", "reaction_options": "Опції реакції", "read_changelog": "Прочитати зміни в оновленні", "readonly_mode_disabled": "Режим лише для читання вимкнено", "readonly_mode_enabled": "Режим лише для читання ввімкнено", "ready_for_upload": "Готово до завантаження", "reassign": "Перепризначити", - "reassigned_assets_to_existing_person": "Перепризначено {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсів}} {name, select, null {існуючій особі} other {{name}}}", - "reassigned_assets_to_new_person": "Перепризначено {count, plural, one {# ресурс} 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 {# файл} other {# файли}} новій особі", + "reassing_hint": "Призначити обрані файли існуючій особі", "recent": "Нещодавно", "recent-albums": "Останні альбоми", "recent_searches": "Нещодавні пошукові запити", @@ -1702,15 +1828,15 @@ "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,11 +1849,11 @@ "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 {# файлу} other {# файлів}}", "rename": "Перейменувати", - "repair": "Ремонт", + "repair": "Відновлення", "repair_no_results_message": "Невідстежувані та відсутні файли будуть відображені тут", "replace_with_upload": "Замінити на завантажене", "repository": "Репозиторій", @@ -1742,17 +1868,17 @@ "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": "Усі дублікати усунуто", "restore": "Відновити", "restore_all": "Відновити все", - "restore_trash_action_prompt": "{count} відновлено з кошика", + "restore_trash_action_prompt": "{count} відновлено зі смітника", "restore_user": "Відновити користувача", - "restored_asset": "Відновлений ресурс", + "restored_asset": "Відновлений файл", "resume": "Продовжити", "resume_paused_jobs": "Відновити {count, plural, one {# призупинене завдання} other {# призупинені завдання}}", "retry_upload": "Повторити завантаження", @@ -1770,9 +1896,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 +1930,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": "Більше результатів немає", @@ -1811,7 +1940,7 @@ "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,46 +1964,52 @@ "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 {Виберіть #} 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_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": "Завантажувати оригінальне зображення", @@ -1889,7 +2024,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": "Загальний прогрес (виконано/загалом)", @@ -1903,7 +2038,7 @@ "settings_require_restart": "Перезавантажте програму для застосування цього налаштування", "settings_saved": "Налаштування збережені", "setup_pin_code": "Налаштувати PIN-код", - "share": "Поділитися", + "share": "Поширити", "share_action_prompt": "{count} фото та відео надіслано", "share_add_photos": "Додати фото", "share_assets_selected": "{count} обрано", @@ -1911,8 +2046,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": "Видалити користувача з альбому", @@ -1938,7 +2073,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} годину", @@ -1966,7 +2101,7 @@ "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 +2117,7 @@ "show_password": "Показати пароль", "show_person_options": "Показати параметри людини", "show_progress_bar": "Показати індикатор прогресу", + "show_schema": "Показати схему", "show_search_options": "Показати параметри пошуку", "show_shared_links": "Показати спільні посилання", "show_slideshow_transition": "Показати перехід слайд-шоу", @@ -1999,10 +2135,12 @@ "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": "Старі фото", @@ -2014,8 +2152,8 @@ "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 +2168,7 @@ "storage": "Сховище", "storage_label": "Мітка для зберігання", "storage_quota": "Обсяг сховища", - "storage_usage": "{used} з {available} доступних", + "storage_usage": "{used} з {available} використано", "submit": "Підтвердити", "success": "Успішно", "suggestions": "Пропозиції", @@ -2054,7 +2192,7 @@ "tag_not_found_question": "Не вдається знайти тег? Створити новий тег.", "tag_people": "Тег людей", "tag_updated": "Оновлено тег: {tag}", - "tagged_assets": "Позначено тегом {count, plural, one {# ресурс} other {# ресурси}}", + "tagged_assets": "Позначено тегом {count, plural, one {# файл} other {# файли}}", "tags": "Теги", "tap_to_run_job": "Торкніться, щоб запустити завдання", "template": "Шаблон", @@ -2062,8 +2200,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 +2213,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": "Кількість секунд для відображення кожного зображення.", @@ -2089,33 +2228,40 @@ "to_multi_select": "для багаторазового вибору", "to_parent": "Повернутись назад", "to_select": "вибрати", - "to_trash": "Кошик", + "to_trash": "Смітник", "toggle_settings": "Перемикання налаштувань", "toggle_theme_description": "Перемкнути тему", "total": "Усього", "total_usage": "Загальне використання", - "trash": "Кошик", - "trash_action_prompt": "{count} переміщено до кошика", + "trash": "Смітник", + "trash_action_prompt": "{count} переміщено до смітника", "trash_all": "Видалити все", "trash_count": "Видалити {count, number}", - "trash_delete_asset": "У кошик/Видалити ресурс", - "trash_emptied": "Кошик очищено", + "trash_delete_asset": "У Смітник/Видалити файл", + "trash_emptied": "Смітник очищено", "trash_no_results_message": "Тут з'являтимуться видалені фото та відео.", "trash_page_delete_all": "Видалити усе", - "trash_page_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_title": "Кошик ({count})", - "trashed_items_will_be_permanently_deleted_after": "Видалені елементи будуть остаточно видалені через {days, plural, one {# день} few {# дні} many {# днів} other {# днів}}.", + "trash_page_select_assets_btn": "Вибрати файли", + "trash_page_title": "Смітник ({count})", + "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_setup_pin_code": "Неможливо налаштувати PIN-код", "unarchive": "Розархівувати", - "unarchive_action_prompt": "{count} вилучено з архіву", + "unarchive_action_prompt": "{count, plural, one {# файл вилучено з архіву} few {# файли вилучено з архіву} other {# файлів вилучено з архіву}}", "unarchived_count": "{count, plural, other {Повернуто з архіву #}}", "undo": "Скасувати", "unfavorite": "Видалити з улюблених", @@ -2123,11 +2269,12 @@ "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 +2285,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_dialog_info": "Бажаєте створити резервну копію вибраних файлів на сервері?", + "upload_dialog_title": "Завантажити Файли", + "upload_error_with_count": "Помилка завантаження для {count, plural, one {# актив} 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_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 +2343,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..7a94ab9482 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", @@ -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,16 @@ "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": "Tên tệp: {file_name}", "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 +1158,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 +1198,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 +1271,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 +1318,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 +1442,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 +1458,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 +1483,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 +1493,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", @@ -1563,6 +1595,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 +1620,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 +1702,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", @@ -1700,7 +1737,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 +1857,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 +1873,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ả", @@ -1982,6 +2024,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 +2152,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 +2168,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 +2189,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 +2236,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 +2248,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 +2266,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 +2279,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/zh_Hant.json b/i18n/zh_Hant.json index bd4073d52c..85fd34a946 100644 --- a/i18n/zh_Hant.json +++ b/i18n/zh_Hant.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} 個項目加入收藏", @@ -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": "較高的解析度能保留更多細節,但編碼時間會更長、檔案大小會更大,並可能降低應用程式的回應速度。", @@ -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_error": "啟動維護模式失敗。", + "maintenance_upload_backup": "上傳數據庫備份文件", + "maintenance_upload_backup_error": "無法上傳備份,它是.sql或.sql.gz格式的文件嗎?", "manage_concurrency": "管理併發", "manage_concurrency_description": "導航到任務頁面以管理任務併發性", "manage_log_settings": "管理日誌設定", @@ -467,10 +487,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,6 +510,7 @@ "albums_default_sort_order_description": "建立新相簿時要初始化項目排序方式。", "albums_feature_description": "一系列可以分享給其他使用者的項目。", "albums_on_device_count": "此裝置有 ({count}) 個相簿", + "albums_selected": "{count, plural, one {# 個已選擇專輯} other {# 個已選擇專輯}}", "all": "全部", "all_albums": "所有相簿", "all_people": "所有人物", @@ -524,10 +547,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": "媒體有未分配的臉孔", @@ -711,6 +736,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 +749,8 @@ "checksum": "校驗和", "choose_matching_people_to_merge": "選擇要合併的相符人物", "city": "城市", + "cleanup_confirm_description": "Immich發現{count}個資產(在{date}之前創建)以安全備份到服務器。是否從此設備中刪除本地副本?", + "cleanup_confirm_prompt_title": "從此裝置刪除?", "clear": "清空", "clear_all": "全部清除", "clear_all_recent_searches": "清除所有最近的搜尋", @@ -787,6 +816,7 @@ "create_album": "建立相簿", "create_album_page_untitled": "未命名", "create_api_key": "創建API金鑰", + "create_first_workflow": "創建第一個工作流", "create_library": "建立媒體庫", "create_link": "建立連結", "create_link_to_share": "建立共享連結", @@ -801,6 +831,7 @@ "create_tag": "建立標籤", "create_tag_description": "建立新標籤。若要建立巢狀標籤,請輸入包含正斜線的完整標籤路徑。", "create_user": "建立使用者", + "create_workflow": "創建工作流", "created": "建立於", "created_at": "建立於", "creating_linked_albums": "建立連結相簿 ...", @@ -867,6 +898,7 @@ "deselect_all": "取消全選", "details": "詳細資訊", "direction": "方向", + "disable": "禁用", "disabled": "已停用", "disallow_edits": "不允許編輯", "discord": "Discord", @@ -929,11 +961,10 @@ "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": "旋轉", "email": "電子郵件", "email_notifications": "Email 通知", "empty_folder": "這個資料夾是空的", @@ -1014,6 +1045,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 +1056,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 +1096,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,6 +1108,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": "排除模式", @@ -1120,14 +1155,16 @@ "features": "功能", "features_in_development": "發展中的特點", "features_setting_description": "管理應用程式功能", - "file_name": "檔案名稱", + "file_name": "檔案名稱:{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": "修復不相符的", @@ -1143,6 +1180,7 @@ "general": "一般", "geolocation_instruction_location": "點選具有 GPS 座標的項目以使用其位置,或直接從地圖中選擇地點", "get_help": "取得協助", + "get_people_error": "獲取人員時出錯", "get_wifiname_error": "無法取得 Wi-Fi 名稱。請確認您已授予必要的權限,並已連線至 Wi-Fi 網路", "getting_started": "開始使用", "go_back": "上一頁", @@ -1175,6 +1213,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,6 +1286,8 @@ "ios_debug_info_processing_ran_at": "於 {dateTime} 執行處理", "items_count": "{count, plural, one {# 個項目} other {# 個項目}}", "jobs": "任務", + "json_editor": "JSON編輯器", + "json_error": "JSON錯誤", "keep": "保留", "keep_all": "全部保留", "keep_this_delete_others": "保留這個,刪除其他", @@ -1416,11 +1457,13 @@ "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_up": "向上移動", "moved_to_archive": "已封存 {count, plural, one {# 個項目} other {# 個項目}}", "moved_to_library": "已移動 {count, plural, one {# 個項目} other {# 個項目}} 至相簿", "moved_to_trash": "已丟進垃圾桶", @@ -1430,6 +1473,7 @@ "my_albums": "我的相簿", "name": "名稱", "name_or_nickname": "名稱或暱稱", + "name_required": "名稱是必填項", "navigate": "導航", "navigate_to_time": "導航到時間", "network_requirement_photos_upload": "使用行動網路流量備份照片", @@ -1454,6 +1498,7 @@ "next": "下一步", "next_memory": "下一張回憶", "no": "否", + "no_actions_added": "尚未添加任何操作", "no_albums_message": "建立相簿來整理照片和影片", "no_albums_with_name_yet": "看來還沒有這個名字的相簿。", "no_albums_yet": "看來您還沒有任何相簿。", @@ -1463,11 +1508,13 @@ "no_cast_devices_found": "找不到 Google Cast 裝置", "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": "未設定位置", @@ -1563,6 +1610,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": "在永久刪除檔案時顯示警告", @@ -1583,10 +1631,12 @@ "permission_onboarding_request": "Immich 需要權限才能檢視您的相片和短片。", "person": "人物", "person_age_months": "{months, plural, one {# 個月} other {# 個月}}前", - "person_age_year_months": "1 年 {months, plural, one {# 個月} other {# 個月}}前", + "person_age_year_months": "1 年 {months, plural, one {# 個月} other {# 個月}}", "person_age_years": "{years, plural, other {# 歲}}", "person_birthdate": "生於 {date}", "person_hidden": "{name}{hidden, select, true {(隱藏)} other {}}", + "person_recognized": "被認可的人", + "person_selected": "已選擇的人", "photo_shared_all_users": "看來您與所有使用者分享了照片,或沒有其他使用者可供分享。", "photos": "照片", "photos_and_videos": "照片及影片", @@ -1667,10 +1717,12 @@ "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": "閱覽變更日誌", "readonly_mode_disabled": "唯讀模式已關閉", @@ -1836,17 +1888,22 @@ "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_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": "全部刪除", @@ -1982,6 +2039,7 @@ "show_password": "顯示密碼", "show_person_options": "顯示人物選項", "show_progress_bar": "顯示進度條", + "show_schema": "顯示架構", "show_search_options": "顯示搜尋選項", "show_shared_links": "顯示共享連結", "show_slideshow_transition": "顯示幻燈片轉場", @@ -2109,6 +2167,13 @@ "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 碼", @@ -2139,13 +2204,14 @@ "unstack": "取消堆疊", "unstack_action_prompt": "{count} 個取消堆疊", "unstacked_assets_count": "已解除堆疊 {count, plural, 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": "是否要將所選項目備份到伺服器?", @@ -2185,6 +2251,7 @@ "utilities": "工具", "validate": "驗證", "validate_endpoint_error": "請輸入有效的 URL", + "validation_error": "驗證錯誤", "variables": "變數", "version": "版本", "version_announcement_closing": "敬祝順心,Alex", @@ -2216,6 +2283,8 @@ "viewer_stack_use_as_main_asset": "作為主項目使用", "viewer_unstack": "取消堆疊", "visibility_changed": "已變更 {count, plural, other {# 位人物}}的可見性", + "visual": "視覺的", + "visual_builder": "視覺構建器", "waiting": "待處理", "waiting_count": "待處理:{count}", "warning": "警告", @@ -2224,13 +2293,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": "您沒有任何共享連結", "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_SIMPLIFIED.json index 6e16116d32..d76191e604 100644 --- a/i18n/zh_SIMPLIFIED.json +++ b/i18n/zh_SIMPLIFIED.json @@ -1,363 +1,383 @@ { "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": "管理外部库页面", - "face_detection": "人脸检测", - "face_detection_description": "使用机器学习检测项目中的人脸(视频只检测其缩略图中的人脸)。选择“刷新”将会(重新)处理所有项目。选择“重置”还会清除所有当前面部数据。选择“缺失”将尚未处理的项目进行排队处理。人脸检测完成后,检测到的人脸将排队进行面部识别,将它们分组到现有的或新的人物中。", - "facial_recognition_job_description": "将检测到的人脸按照人物分组。这一步将在人脸检测完成后执行。选择“重置”将会(重新)分组所有人脸。选择“缺失”将尚未分配的人脸置于队列中。", - "failed_job_command": "{command}命令执行失败的任务:{job}", - "force_delete_user_warning": "警告:这将立即移除用户以及其所有项目。该操作无法撤销且文件无法恢复。", + "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": "警告:此操作将立即删除该用户及其所有资源。此操作不可撤销,且文件无法恢复。", "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_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": "与邮件服务器进行身份验证时使用的密码", - "notification_email_port_description": "邮件服务器端口(例如 25、465 或 587)", + "notification_email_ignore_certificate_errors_description": "忽略 TLS 证书验证错误(不推荐)", + "notification_email_password_description": "连接邮件服务器进行身份验证时使用的密码", + "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": "与邮件服务器进行身份验证时使用的用户名", + "notification_email_test_email_failed": "发送测试邮件失败,请检查您的配置信息", + "notification_email_test_email_sent": "已向 {email} 发送测试邮件,请查收。", + "notification_email_username_description": "连接邮件服务器进行身份验证时使用的用户名", "notification_enable_email_notifications": "启用邮件通知", "notification_settings": "通知设置", - "notification_settings_description": "管理通知设置,包括邮件", + "notification_settings_description": "管理通知设置,包括邮件通知", "oauth_auto_launch": "自动启动", - "oauth_auto_launch_description": "在登录页面自动启动 OAuth 登录", + "oauth_auto_launch_description": "进入登录页面时,自动开始 OAuth 登录流程", "oauth_auto_register": "自动注册", - "oauth_auto_register_description": "使用 OAuth 登录后自动注册为新用户", - "oauth_button_text": "按钮文本", - "oauth_client_secret_description": "如果 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": "所有路径均已成功验证", + "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": "启用哈希校验,如果您不知道此项的作用请不要禁用此功能", - "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": "已启用哈希校验", + "storage_template_hash_verification_enabled_description": "开启哈希校验功能。除非你清楚关闭后的后果,否则请勿关闭", + "storage_template_migration": "存储模板迁移", + "storage_template_migration_description": "将当前 {template} 应用于已上传的资源", + "storage_template_migration_info": "存储模板会将所有文件扩展名转换为小写。模板更改仅对新上传的资源生效。若要将模板回溯应用于已上传的资源,请运行 {job}。", + "storage_template_migration_job": "存储模板迁移任务", + "storage_template_more_details": "有关此功能的更多详细信息,请参阅 存储模板 及其 含义", + "storage_template_onboarding_description_v2": "启用后,此功能将根据用户定义的模板自动整理文件。更多信息,请参阅 文档。", + "storage_template_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": "硬件加速", @@ -374,48 +394,48 @@ "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} 已成功删除。", @@ -423,74 +443,81 @@ "version_check_enabled_description": "启用版本检测", "version_check_implications": "版本检查功能依赖于与 github.com 的定期通信", "version_check_settings": "版本检查", - "version_check_settings_description": "启用或禁用新版本通知", - "video_conversion_job": "视频转码", + "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 证书验证(该选项适用于使用自签名证书的服务器)。", - "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": "该应用密钥只会显示一次。请确保在关闭窗口前复制下来。", @@ -511,38 +541,40 @@ "app_settings": "应用设置", "app_stores": "应用商店", "app_update_available": "应用程序更新可用", - "appears_in": "出现于", + "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)", "archived": "已归档", "archived_count": "{count, plural, other {已归档 # 项}}", - "are_these_the_same_person": "他们是同一位人吗?", + "are_these_the_same_person": "他们是同一个人吗?", "are_you_sure_to_do_this": "确定执行此操作?", - "asset_action_delete_err_read_only": "无法删除只读项目,跳过", - "asset_action_share_err_offline": "无法获取离线项目,跳过", + "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_offline": "资产脱机", + "asset_offline_description": "磁盘上已找不到该外部资产。请联系您的 Immich 管理员寻求帮助。", + "asset_restored_successfully": "已成功恢复所有资产", "asset_skipped": "已跳过", "asset_skipped_in_trash": "已回收", "asset_trashed": "资产已被删除", @@ -551,35 +583,35 @@ "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": "已永久移除 {count} 个项目", + "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_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 {个资产}} 已在相册中", "authorized_devices": "已授权设备", "automatic_endpoint_switching_subtitle": "连接指定 Wi-Fi 时使用本地网络,否则使用外部网络", "automatic_endpoint_switching_title": "自动切换 URL", "autoplay_slideshow": "自动播放幻灯片", "back": "返回", - "back_close_deselect": "返回、关闭或反选", + "back_close_deselect": "返回、关闭或取消选择", "background_backup_running_error": "后台备份正在运行,无法启动手动备份", "background_location_permission": "后台定位权限", "background_location_permission_content": "为确保后台运行时自动切换网络,需授予 Immich *始终允许精确定位* 权限,以识别 Wi-Fi 网络名称", @@ -587,7 +619,7 @@ "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": "总计", @@ -596,11 +628,11 @@ "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_upload_failure_notification": "“{filename}”上传失败", "backup_controller_page_albums": "备份相册", "backup_controller_page_background_app_refresh_disabled_content": "要使用后台备份功能,请在“设置”>“常规”>“后台应用刷新”中启用后台应用程序刷新。", "backup_controller_page_background_app_refresh_disabled_title": "后台应用刷新已禁用", @@ -611,8 +643,8 @@ "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 +654,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}]", @@ -641,7 +673,7 @@ "backup_controller_page_turn_off": "关闭前台备份", "backup_controller_page_turn_on": "开启前台备份", "backup_controller_page_uploading_file_info": "正在上传中的文件信息", - "backup_err_only_album": "不能移除唯一的一个相册", + "backup_err_only_album": "无法删除唯一的相册", "backup_error_sync_failed": "同步失败。无法处理备份。", "backup_info_card_assets": "项", "backup_manual_cancelled": "已取消", @@ -664,16 +696,16 @@ "bugs_and_feature_requests": "Bug 与功能请求", "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_duplicated_assets_clear_button": "清除", "cache_settings_duplicated_assets_subtitle": "应用程序忽略的照片和视频", - "cache_settings_duplicated_assets_title": "重复项目({count})", - "cache_settings_statistics_album": "图库缩略图", + "cache_settings_duplicated_assets_title": "重复资产({count})", + "cache_settings_statistics_album": "资产库缩略图", "cache_settings_statistics_full": "完整图像", "cache_settings_statistics_shared": "共享相册缩略图", "cache_settings_statistics_thumbnail": "缩略图", @@ -711,17 +743,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": "清除所有最近搜索", @@ -787,31 +833,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 +884,7 @@ "deduplication_criteria_1": "图像大小(字节)", "deduplication_criteria_2": "EXIF 数据计数", "deduplication_info": "重复数据删除汇总", - "deduplication_info_description": "要自动预选项目并批量删除重复项,我们会考虑:", + "deduplication_info_description": "要自动预选资产并批量删除重复项,我们会考虑:", "default_locale": "默认地区", "default_locale_description": "根据您的浏览器地区设置日期和数字显示格式", "delete": "删除", @@ -837,7 +892,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 +901,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 +915,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 +938,7 @@ "documentation": "帮助文档", "done": "完成", "download": "下载", - "download_action_prompt": "正在下载 {count} 个项目", + "download_action_prompt": "正在下载 {count} 个资产", "download_canceled": "下载已取消", "download_complete": "下载完成", "download_enqueue": "已加入下载队列", @@ -892,6 +948,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 +958,7 @@ "download_waiting_to_retry": "等待重试", "downloading": "下载中", "downloading_asset_filename": "下载项目“{filename}”", + "downloading_from_icloud": "从iCloud下载", "downloading_media": "正在下载媒体", "drop_files_to_upload": "拖放文件以上传", "duplicates": "重复项", @@ -929,11 +987,17 @@ "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_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 +1016,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 +1067,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 +1081,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 +1092,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 +1113,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 +1129,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 +1163,7 @@ "experimental_settings_new_asset_list_title": "启用实验性照片网格", "experimental_settings_subtitle": "使用风险自负!", "experimental_settings_title": "实验性功能", - "expire_after": "有效期至", + "expire_after": "过期时间", "expired": "已过期", "expires_date": "过期于 {date}", "explore": "探索", @@ -1100,8 +1172,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 +1192,16 @@ "features": "功能", "features_in_development": "开发中的功能", "features_setting_description": "管理 App 功能", - "file_name": "文件名", - "file_name_or_extension": "文件名", + "file_name": "文件名:{file_name}", + "file_name_or_extension": "文件名或扩展名", "file_size": "大小", "filename": "文件名", "filetype": "文件类型", - "filter": "滤镜", - "filter_people": "过滤人物", + "filter": "筛选器", + "filter_description": "目标项目筛选条件", + "filter_people": "筛选人物", "filter_places": "筛选地点", + "filters": "筛选器", "find_them_fast": "按名称快速搜索", "first": "第一个", "fix_incorrect_match": "修复不正确的匹配", @@ -1137,17 +1211,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": "获取权限", @@ -1175,22 +1253,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 +1298,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 +1326,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 +1356,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 +1377,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 +1431,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 +1461,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 +1486,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 +1496,7 @@ "matches": "匹配", "matching_assets": "匹配资产", "media_type": "媒体类型", - "memories": "回忆", + "memories": "那年今日", "memories_all_caught_up": "已全部看完", "memories_check_back_tomorrow": "明天再看", "memories_setting_description": "管理回忆中的内容", @@ -1408,6 +1514,8 @@ "minimize": "最小化", "minute": "分", "minutes": "分钟", + "mirror_horizontal": "水平", + "mirror_vertical": "垂直", "missing": "缺失", "mobile_app": "手机APP", "mobile_app_download_onboarding_note": "下载移动应用以访问这些选项", @@ -1416,11 +1524,14 @@ "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 +1541,7 @@ "my_albums": "我的相册", "name": "名称", "name_or_nickname": "名称或昵称", + "name_required": "名称是必填项", "navigate": "导航", "navigate_to_time": "导航至时间", "network_requirement_photos_upload": "使用蜂窝数据备份照片", @@ -1454,20 +1566,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 +1597,12 @@ "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": "提示:要将存储标签应用于之前上传的项目,需要运行", + "note_apply_storage_label_to_previously_uploaded assets": "提示:要将存储标签应用于之前上传的项目,请运行此", "notes": "提示", "nothing_here_yet": "这里什么都没有", "notification_permission_dialog_content": "要启用通知,请转到“设置”,并选择“允许”。", @@ -1517,7 +1634,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 +1648,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 +1680,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 +1705,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 +1739,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 +1777,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 +1788,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": "只读模式已启用", @@ -1701,7 +1824,7 @@ "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 +1893,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 +1927,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 +1962,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 +2097,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 +2114,7 @@ "show_password": "显示密码", "show_person_options": "显示人物选项", "show_progress_bar": "显示进度条", + "show_schema": "显示架构", "show_search_options": "显示搜索选项", "show_shared_links": "显示共享链接", "show_slideshow_transition": "显示幻灯片过渡效果", @@ -1999,6 +2132,8 @@ "skip_to_folders": "跳转到文件夹", "skip_to_tags": "跳转到标签", "slideshow": "幻灯片放映", + "slideshow_repeat": "重复幻灯片", + "slideshow_repeat_description": "幻灯片结束后循环播放", "slideshow_settings": "放映设置", "sort_albums_by": "相册排序依据...", "sort_created": "创建日期", @@ -2075,6 +2210,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 +2245,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 +2266,7 @@ "unhide_person": "显示人物", "unknown": "未知", "unknown_country": "未知的国家", + "unknown_date": "未知日期", "unknown_year": "未知年份", "unlimited": "无限制", "unlink_motion_video": "取消链接动态视频", @@ -2139,13 +2283,14 @@ "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": "是否要将所选项目备份到服务器?", @@ -2185,6 +2330,7 @@ "utilities": "实用工具", "validate": "验证", "validate_endpoint_error": "请输入有效的 URL", + "validation_error": "验证错误", "variables": "变量", "version": "版本", "version_announcement_closing": "您的朋友,Alex", @@ -2196,6 +2342,7 @@ "video_hover_setting_description": "当鼠标悬停在项目上时播放视频缩略图。即使禁用了此功能,也可以通过将鼠标悬停在播放图标上来开始播放。", "videos": "视频", "videos_count": "{count, plural, one {#个视频} other {#个视频}}", + "videos_only": "仅视频", "view": "查看", "view_album": "查看相册", "view_all": "查看全部", @@ -2216,7 +2363,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 +2373,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/machine-learning/Dockerfile b/machine-learning/Dockerfile index 32b2bc6db0..dfc217c118 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -92,14 +92,14 @@ FROM python:3.13-slim-trixie@sha256:0222b795db95bf7412cede36ab46a266cfb31f632e64 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/v2.24.8/intel-igc-core-2_2.24.8+20344_amd64.deb && \ - wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.24.8/intel-igc-opencl-2_2.24.8+20344_amd64.deb && \ - wget -nv https://github.com/intel/compute-runtime/releases/download/25.48.36300.8/intel-opencl-icd_25.48.36300.8-0_amd64.deb && \ + wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.27.10/intel-igc-core-2_2.27.10+20617_amd64.deb && \ + wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.27.10/intel-igc-opencl-2_2.27.10+20617_amd64.deb && \ + wget -nv https://github.com/intel/compute-runtime/releases/download/26.01.36711.4/intel-opencl-icd_26.01.36711.4-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/25.48.36300.8/libigdgmm12_22.8.2_amd64.deb && \ + wget -nv https://github.com/intel/compute-runtime/releases/download/26.01.36711.4/libigdgmm12_22.9.0_amd64.deb && \ dpkg -i *.deb && \ rm *.deb && \ apt-get remove wget -yqq && \ diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 04a10aa09b..ae6d7c0e2c 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "immich-ml" -version = "2.4.1" +version = "2.5.2" description = "" authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }] requires-python = ">=3.11,<4.0" @@ -41,7 +41,6 @@ types = [ "types-ujson>=5.10.0.20240515", ] lint = [ - "black>=23.3.0", "mypy>=1.3.0", "ruff>=0.0.272", { include-group = "types" }, @@ -93,9 +92,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/uv.lock b/machine-learning/uv.lock index e040dcb5f7..4ab5dbff36 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -85,43 +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.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "pytokens" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/ad/7ac0d0e1e0612788dbc48e62aef8a8e8feffac7eb3d787db4e43b8462fa8/black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a", size = 1877003, upload-time = "2025-12-08T01:43:29.967Z" }, - { url = "https://files.pythonhosted.org/packages/e8/dd/a237e9f565f3617a88b49284b59cbca2a4f56ebe68676c1aad0ce36a54a7/black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be", size = 1712639, upload-time = "2025-12-08T01:52:46.756Z" }, - { url = "https://files.pythonhosted.org/packages/12/80/e187079df1ea4c12a0c63282ddd8b81d5107db6d642f7d7b75a6bcd6fc21/black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b", size = 1758143, upload-time = "2025-12-08T01:45:29.137Z" }, - { url = "https://files.pythonhosted.org/packages/93/b5/3096ccee4f29dc2c3aac57274326c4d2d929a77e629f695f544e159bfae4/black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5", size = 1420698, upload-time = "2025-12-08T01:45:53.379Z" }, - { url = "https://files.pythonhosted.org/packages/7e/39/f81c0ffbc25ffbe61c7d0385bf277e62ffc3e52f5ee668d7369d9854fadf/black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655", size = 1229317, upload-time = "2025-12-08T01:46:35.606Z" }, - { url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" }, - { url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" }, - { url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" }, - { url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" }, - { url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" }, - { url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" }, - { url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" }, - { url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" }, - { url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" }, - { url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" }, - { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" }, -] - [[package]] name = "blinker" version = "1.7.0" @@ -919,7 +882,7 @@ wheels = [ [[package]] name = "immich-ml" -version = "2.4.1" +version = "2.5.2" source = { editable = "." } dependencies = [ { name = "aiocache" }, @@ -961,7 +924,6 @@ rknn = [ [package.dev-dependencies] dev = [ - { name = "black" }, { name = "httpx" }, { name = "locust" }, { name = "mypy" }, @@ -977,7 +939,6 @@ dev = [ { name = "types-ujson" }, ] lint = [ - { name = "black" }, { name = "mypy" }, { name = "ruff" }, { name = "types-pyyaml" }, @@ -1031,7 +992,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" }, @@ -1047,7 +1007,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" }, @@ -2232,15 +2191,6 @@ 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 = "311" 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 deca397081..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 @@ -92,6 +89,5 @@ sed -i "s/\"android\.injected\.version\.code\" => $CURRENT_MOBILE,/\"android\.in 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 a4f597662a..0e7237be20 100644 --- a/mise.toml +++ b/mise.toml @@ -1,12 +1,25 @@ experimental_monorepo_root = true +[monorepo] +config_roots = [ + "plugins", + "server", + "cli", + "deployment", + "mobile", + "e2e", + "web", + "docs", + ".github", +] + [tools] -node = "24.12.0" +node = "24.13.0" flutter = "3.35.7" -pnpm = "10.27.0" -terragrunt = "0.93.10" -opentofu = "1.10.7" -java = "25.0.1" +pnpm = "10.28.0" +terragrunt = "0.98.0" +opentofu = "1.11.4" +java = "21.0.2" [tools."github:CQLabs/homebrew-dcm"] version = "1.30.0" 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..3360617a3d 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" diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index c6e04e5a10..0d4925077a 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -117,6 +117,9 @@ + 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 index 44d2aee2ce..6c22f9e284 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/HttpSSLOptionsPlugin.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/HttpSSLOptionsPlugin.kt @@ -2,6 +2,7 @@ package app.alextran.immich import android.annotation.SuppressLint import android.content.Context +import app.alextran.immich.core.SSLConfig import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall @@ -51,15 +52,18 @@ class HttpSSLOptionsPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { when (call.method) { "apply" -> { val args = call.arguments>()!! + val allowSelfSigned = args[0] as Boolean + val serverHost = args[1] as? String + val clientCertHash = (args[2] as? ByteArray) var tm: Array? = null - if (args[0] as Boolean) { - tm = arrayOf(AllowSelfSignedTrustManager(args[1] as? String)) + if (allowSelfSigned) { + tm = arrayOf(AllowSelfSignedTrustManager(serverHost)) } var km: Array? = null - if (args[2] != null) { - val cert = ByteArrayInputStream(args[2] as ByteArray) + if (clientCertHash != null) { + val cert = ByteArrayInputStream(clientCertHash) val password = (args[3] as String).toCharArray() val keyStore = KeyStore.getInstance("PKCS12") keyStore.load(cert, password) @@ -69,6 +73,9 @@ class HttpSSLOptionsPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { km = keyManagerFactory.keyManagers } + // Update shared SSL config for OkHttp and other HTTP clients + SSLConfig.apply(km, tm, allowSelfSigned, serverHost, clientCertHash?.contentHashCode() ?: 0) + val sslContext = SSLContext.getInstance("TLS") sslContext.init(km, tm, null) HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) 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..08790d9772 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 @@ -10,8 +10,10 @@ import app.alextran.immich.background.BackgroundWorkerLockApi import app.alextran.immich.connectivity.ConnectivityApi import app.alextran.immich.connectivity.ConnectivityApiImpl import app.alextran.immich.core.ImmichPlugin -import app.alextran.immich.images.ThumbnailApi -import app.alextran.immich.images.ThumbnailsImpl +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 @@ -36,7 +38,9 @@ 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)) 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/SSLConfig.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/SSLConfig.kt new file mode 100644 index 0000000000..f62042cd99 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/SSLConfig.kt @@ -0,0 +1,73 @@ +package app.alextran.immich.core + +import java.security.KeyStore +import javax.net.ssl.KeyManager +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager + +/** + * Shared SSL configuration for OkHttp and HttpsURLConnection. + * Stores the SSLSocketFactory and X509TrustManager configured by HttpSSLOptionsPlugin. + */ +object SSLConfig { + var sslSocketFactory: SSLSocketFactory? = null + private set + + var trustManager: X509TrustManager? = null + private set + + var requiresCustomSSL: Boolean = false + private set + + private val listeners = mutableListOf<() -> Unit>() + private var configHash: Int = 0 + + fun addListener(listener: () -> Unit) { + listeners.add(listener) + } + + fun apply( + keyManagers: Array?, + trustManagers: Array?, + allowSelfSigned: Boolean, + serverHost: String?, + clientCertHash: Int + ) { + synchronized(this) { + val newHash = computeHash(allowSelfSigned, serverHost, clientCertHash) + val newRequiresCustomSSL = allowSelfSigned || keyManagers != null + if (newHash == configHash && sslSocketFactory != null && requiresCustomSSL == newRequiresCustomSSL) { + return // Config unchanged, skip + } + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(keyManagers, trustManagers, null) + sslSocketFactory = sslContext.socketFactory + trustManager = trustManagers?.filterIsInstance()?.firstOrNull() + ?: getDefaultTrustManager() + requiresCustomSSL = newRequiresCustomSSL + configHash = newHash + notifyListeners() + } + } + + private fun computeHash(allowSelfSigned: Boolean, serverHost: String?, clientCertHash: Int): Int { + var result = allowSelfSigned.hashCode() + result = 31 * result + (serverHost?.hashCode() ?: 0) + result = 31 * result + clientCertHash + return result + } + + private fun notifyListeners() { + listeners.forEach { it() } + } + + private fun getDefaultTrustManager(): X509TrustManager { + val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + factory.init(null as KeyStore?) + return factory.trustManagers.filterIsInstance().first() + } +} 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 77% 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..5b95daf38b 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, 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,13 @@ 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> -> + api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg) { 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 +97,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 +115,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 +123,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 75% 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..50ff11b0c2 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,7 +11,8 @@ 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.util.concurrent.Executors import com.bumptech.glide.Glide @@ -26,10 +27,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) + recycle() + return mapOf( + "pointer" to pointer, + "width" to width.toLong(), + "height" to height.toLong(), + "rowBytes" to (width * 4).toLong() + ) + } catch (e: Exception) { + NativeBuffer.free(pointer) + recycle() + throw e + } +} + +class LocalImagesImpl(context: Context) : LocalImageApi { private val ctx: Context = context.applicationContext private val resolver: ContentResolver = ctx.contentResolver private val requestThread = Executors.newSingleThreadExecutor() @@ -38,21 +71,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 +83,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,7 +99,7 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi { width: Long, height: Long, isVideo: Boolean, - callback: (Result>) -> Unit + callback: (Result?>) -> Unit ) { val signal = CancellationSignal() val task = threadPool.submit { @@ -98,7 +119,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() @@ -117,7 +138,7 @@ class ThumbnailsImpl(context: Context) : ThumbnailApi { width: Long, height: Long, isVideo: Boolean, - callback: (Result>) -> Unit, + callback: (Result?>) -> Unit, signal: CancellationSignal ) { signal.throwIfCanceled() @@ -131,31 +152,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 +193,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..0e3cf19657 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt @@ -0,0 +1,123 @@ +// 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, 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 + api.requestImage(urlArg, headersArg, requestIdArg) { 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..6800b45a70 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt @@ -0,0 +1,530 @@ +package app.alextran.immich.images + +import android.content.Context +import android.os.CancellationSignal +import android.os.OperationCanceledException +import app.alextran.immich.BuildConfig +import app.alextran.immich.INITIAL_BUFFER_SIZE +import app.alextran.immich.NativeBuffer +import app.alextran.immich.NativeByteBuffer +import app.alextran.immich.core.SSLConfig +import kotlinx.coroutines.* +import okhttp3.Cache +import okhttp3.Call +import okhttp3.Callback +import okhttp3.ConnectionPool +import okhttp3.Dispatcher +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 +import java.util.concurrent.TimeUnit +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509TrustManager + + +private const val USER_AGENT = "Immich_Android_${BuildConfig.VERSION_NAME}" +private const val MAX_REQUESTS_PER_HOST = 64 +private const val KEEP_ALIVE_CONNECTIONS = 10 +private const val KEEP_ALIVE_DURATION_MINUTES = 5L +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, + 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() + SSLConfig.addListener(::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 + if (oldFetcher is OkHttpImageFetcher && SSLConfig.requiresCustomSSL) { + fetcher = oldFetcher.reconfigure(SSLConfig.sslSocketFactory, SSLConfig.trustManager) + return + } + fetcher = build() + oldFetcher.drain() + } + } + + private fun build(): ImageFetcher { + return if (SSLConfig.requiresCustomSSL) { + OkHttpImageFetcher.create(cacheDir, SSLConfig.sslSocketFactory, SSLConfig.trustManager) + } 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, + sslSocketFactory: SSLSocketFactory?, + trustManager: X509TrustManager?, + ): OkHttpImageFetcher { + val dir = File(cacheDir, "okhttp") + val connectionPool = ConnectionPool( + maxIdleConnections = KEEP_ALIVE_CONNECTIONS, + keepAliveDuration = KEEP_ALIVE_DURATION_MINUTES, + timeUnit = TimeUnit.MINUTES + ) + + val builder = OkHttpClient.Builder() + .addInterceptor { chain -> + chain.proceed( + chain.request().newBuilder() + .header("User-Agent", USER_AGENT) + .build() + ) + } + .dispatcher(Dispatcher().apply { maxRequestsPerHost = MAX_REQUESTS_PER_HOST }) + .connectionPool(connectionPool) + .cache(Cache(File(dir, "thumbnails"), CACHE_SIZE_BYTES)) + + if (sslSocketFactory != null && trustManager != null) { + builder.sslSocketFactory(sslSocketFactory, trustManager) + } + + return OkHttpImageFetcher(builder.build()) + } + } + + fun reconfigure( + sslSocketFactory: SSLSocketFactory?, + trustManager: X509TrustManager?, + ): OkHttpImageFetcher { + val builder = client.newBuilder() + if (sslSocketFactory != null && trustManager != null) { + builder.sslSocketFactory(sslSocketFactory, trustManager) + } + // Evict idle connections using old SSL config + client.connectionPool.evictAll() + return OkHttpImageFetcher(builder.build()) + } + + 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 + } + client.connectionPool.evictAll() + 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..b59f47a1d6 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 @@ -252,6 +252,40 @@ 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) { @@ -275,6 +309,11 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { HashResult.fromList(it) } } + 133.toByte() -> { + return (readValue(buffer) as? List)?.let { + CloudIdResult.fromList(it) + } + } else -> super.readValueOfType(type, buffer) } } @@ -296,6 +335,10 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { stream.write(132) writeValue(stream, value.toList()) } + is CloudIdResult -> { + stream.write(133) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -315,6 +358,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 +552,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..1b04fa50eb 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 @@ -14,6 +14,7 @@ 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,7 +22,6 @@ 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() @@ -298,7 +298,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 +316,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/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index b20b29c4ee..006a75c139 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" => 3030, - "android.injected.version.name" => "2.4.1", + "android.injected.version.code" => 3033, + "android.injected.version.name" => "2.5.2", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/drift_schemas/main/drift_schema_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/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/Podfile.lock b/mobile/ios/Podfile.lock index d869aa9c08..77caaeceef 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -6,6 +6,9 @@ PODS: - FlutterMacOS - connectivity_plus (0.0.1): - Flutter + - cupertino_http (0.0.1): + - Flutter + - FlutterMacOS - device_info_plus (0.0.1): - Flutter - DKImagePickerController/Core (4.3.9): @@ -77,6 +80,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): @@ -136,6 +141,7 @@ 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`) @@ -154,6 +160,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`) @@ -184,6 +191,8 @@ 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: @@ -220,6 +229,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,6 +260,7 @@ SPEC CHECKSUMS: background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 @@ -270,6 +282,7 @@ 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 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 599e7990f4..991f075ad9 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -29,9 +29,11 @@ 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 */; }; 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 +120,11 @@ 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 = ""; }; 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 +325,11 @@ FED3B1952E253E9B0030FD97 /* Images */ = { isa = PBXGroup; children = ( + 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 +606,14 @@ 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 */, 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 +741,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 +885,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 +915,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 +949,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 +992,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 +1032,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 +1071,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 +1115,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 +1156,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/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 4e4cb2ed13..60f97b6645 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -53,8 +53,10 @@ import UIKit public static func registerPlugins(with engine: FlutterEngine) { 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()) } public static func cancelPlugins(with engine: FlutterEngine) { 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/Images/Thumbnails.g.swift b/mobile/ios/Runner/Images/LocalImages.g.swift similarity index 68% rename from mobile/ios/Runner/Images/Thumbnails.g.swift rename to mobile/ios/Runner/Images/LocalImages.g.swift index fbaef294d3..d417f10222 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, 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?] @@ -102,22 +102,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/ThumbnailsImpl.swift b/mobile/ios/Runner/Images/LocalImagesImpl.swift similarity index 58% rename from mobile/ios/Runner/Images/ThumbnailsImpl.swift rename to mobile/ios/Runner/Images/LocalImagesImpl.swift index 452ca62377..4f2090443a 100644 --- a/mobile/ios/Runner/Images/ThumbnailsImpl.swift +++ b/mobile/ios/Runner/Images/LocalImagesImpl.swift @@ -1,19 +1,19 @@ -import CryptoKit +import Accelerate import Flutter import MobileCoreServices import Photos -class Request { +class LocalImageRequest { weak var workItem: DispatchWorkItem? var isCancelled = false - let callback: (Result<[String: Int64], any Error>) -> Void + let callback: (Result<[String: Int64]?, any Error>) -> Void - init(callback: @escaping (Result<[String: Int64], any Error>) -> Void) { + init(callback: @escaping (Result<[String: Int64]?, any Error>) -> Void) { self.callback = callback } } -class ThumbnailApiImpl: ThumbnailApi { +class LocalImageApiImpl: LocalImageApi { private static let imageManager = PHImageManager.default() private static let fetchOptions = { let fetchOptions = PHFetchOptions() @@ -36,47 +36,39 @@ class ThumbnailApiImpl: ThumbnailApi { 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 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 cancelledResult = Result<[String: Int64]?, any Error>.success(nil) 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)])) + 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, completion: @escaping (Result<[String: Int64], any Error>) -> Void) { - let request = Request(callback: completion) + func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) { + let request = LocalImageRequest(callback: completion) let item = DispatchWorkItem { if request.isCancelled { return completion(Self.cancelledResult) @@ -93,7 +85,7 @@ class ThumbnailApiImpl: ThumbnailApi { guard let asset = Self.requestAsset(assetId: assetId) else { - Self.removeRequest(requestId: requestId) + Self.remove(requestId: requestId) completion(.failure(PigeonError(code: "", message: "Could not get asset data for \(assetId)", details: nil))) return } @@ -119,70 +111,54 @@ class ThumbnailApiImpl: ThumbnailApi { guard let image = image, let cgImage = image.cgImage else { - Self.removeRequest(requestId: requestId) + Self.remove(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))) + do { + let buffer = try vImage_Buffer(cgImage: cgImage, format: Self.rgbaFormat) + + if request.isCancelled { + buffer.free() + return completion(Self.cancelledResult) + } + + request.callback(.success([ + "pointer": Int64(Int(bitPattern: buffer.data)), + "width": Int64(buffer.width), + "height": Int64(buffer.height), + "rowBytes": Int64(buffer.rowBytes) + ])) + print("Successful response for \(requestId)") + Self.remove(requestId: requestId) + } catch { + Self.remove(requestId: requestId) + return completion(.failure(PigeonError(code: "", message: "Failed to convert image for \(assetId): \(error)", 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.add(requestId: requestId, request: request) Self.processingQueue.async(execute: item) } - func cancelImageRequest(requestId: Int64) { - Self.cancelRequest(requestId: requestId) + func cancelRequest(requestId: Int64) { + Self.cancel(requestId: requestId) } - private static func addRequest(requestId: Int64, request: Request) -> Void { + private static func add(requestId: Int64, request: LocalImageRequest) -> Void { requestQueue.sync { requests[requestId] = request } } - private static func removeRequest(requestId: Int64) -> Void { + private static func remove(requestId: Int64) -> Void { requestQueue.sync { requests[requestId] = nil } } - private static func cancelRequest(requestId: Int64) -> Void { + private static func cancel(requestId: Int64) -> Void { requestQueue.async { guard let request = requests.removeValue(forKey: requestId) else { return } request.isCancelled = true @@ -203,9 +179,4 @@ class ThumbnailApiImpl: ThumbnailApi { assetQueue.async { assetCache.setObject(asset, forKey: assetId as NSString) } return asset } - - func waitForActiveState() { - Self.activitySemaphore.wait() - Self.activitySemaphore.signal() - } } diff --git a/mobile/ios/Runner/Images/RemoteImages.g.swift b/mobile/ios/Runner/Images/RemoteImages.g.swift new file mode 100644 index 0000000000..fc83b09d4b --- /dev/null +++ b/mobile/ios/Runner/Images/RemoteImages.g.swift @@ -0,0 +1,134 @@ +// 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, 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 + api.requestImage(url: urlArg, headers: headersArg, requestId: requestIdArg) { 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..d59204b96e --- /dev/null +++ b/mobile/ios/Runner/Images/RemoteImagesImpl.swift @@ -0,0 +1,186 @@ +import Accelerate +import Flutter +import MobileCoreServices +import Photos + +class RemoteImageRequest { + weak var task: URLSessionDataTask? + let id: Int64 + var isCancelled = false + var data: CFMutableData? + 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.data = nil + self.completion = completion + } +} + +class RemoteImageApiImpl: NSObject, RemoteImageApi { + private static let delegate = RemoteImageApiDelegate() + static let session = { + let cacheDir = FileManager.default.temporaryDirectory.appendingPathComponent("thumbnails", isDirectory: true) + let config = URLSessionConfiguration.default + config.requestCachePolicy = .returnCacheDataElseLoad + let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown" + config.httpAdditionalHeaders = ["User-Agent": "Immich_iOS_\(version)"] + try! FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true) + config.urlCache = URLCache( + memoryCapacity: 0, + diskCapacity: 1 << 30, + directory: cacheDir + ) + config.httpMaximumConnectionsPerHost = 64 + return URLSession(configuration: config, delegate: delegate, delegateQueue: nil) + }() + + func requestImage(url: String, headers: [String : String], requestId: Int64, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) { + var urlRequest = URLRequest(url: URL(string: url)!) + for (key, value) in headers { + urlRequest.setValue(value, forHTTPHeaderField: key) + } + let task = Self.session.dataTask(with: urlRequest) + + let imageRequest = RemoteImageRequest(id: requestId, task: task, completion: completion) + Self.delegate.add(taskId: task.taskIdentifier, request: imageRequest) + + task.resume() + } + + func cancelRequest(requestId: Int64) { + Self.delegate.cancel(requestId: requestId) + } + + func clearCache(completion: @escaping (Result) -> Void) { + Task { + let cache = Self.session.configuration.urlCache! + let cacheSize = Int64(cache.currentDiskUsage) + cache.removeAllCachedResponses() + completion(.success(cacheSize)) + } + } +} + +class RemoteImageApiDelegate: NSObject, URLSessionDataDelegate { + private static let requestQueue = DispatchQueue(label: "thumbnail.requests", qos: .userInitiated, attributes: .concurrent) + private static var rgbaFormat = vImage_CGImageFormat( + bitsPerComponent: 8, + bitsPerPixel: 32, + colorSpace: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue), + renderingIntent: .perceptual + )! + private static var requestByTaskId = [Int: RemoteImageRequest]() + private static var taskIdByRequestId = [Int64: Int]() + private static let cancelledResult = Result<[String: Int64]?, any Error>.success(nil) + private static let decodeOptions = [ + kCGImageSourceShouldCache: false, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceCreateThumbnailFromImageAlways: true + ] as CFDictionary + + func urlSession( + _ session: URLSession, dataTask: URLSessionDataTask, + didReceive response: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void + ) { + guard let request = get(taskId: dataTask.taskIdentifier) + else { + return completionHandler(.cancel) + } + + let capacity = max(Int(response.expectedContentLength), 0) + request.data = CFDataCreateMutable(nil, capacity) + + completionHandler(.allow) + } + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, + didReceive data: Data) { + guard let request = get(taskId: dataTask.taskIdentifier) else { return } + + data.withUnsafeBytes { bytes in + CFDataAppendBytes(request.data, bytes.bindMemory(to: UInt8.self).baseAddress, data.count) + } + } + + func urlSession(_ session: URLSession, task: URLSessionTask, + didCompleteWithError error: Error?) { + guard let request = get(taskId: task.taskIdentifier) else { return } + + defer { remove(taskId: task.taskIdentifier, requestId: request.id) } + + if let error = error { + if request.isCancelled || (error as NSError).code == NSURLErrorCancelled { + return request.completion(Self.cancelledResult) + } + return request.completion(.failure(error)) + } + + if request.isCancelled { + return request.completion(Self.cancelledResult) + } + + guard let data = request.data else { + return request.completion(.failure(PigeonError(code: "", message: "No data received", details: nil))) + } + + guard let imageSource = CGImageSourceCreateWithData(data, nil), + let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, Self.decodeOptions) else { + return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil))) + } + + if request.isCancelled { + return request.completion(Self.cancelledResult) + } + + do { + let buffer = try vImage_Buffer(cgImage: cgImage, format: Self.rgbaFormat) + + if request.isCancelled { + buffer.free() + return request.completion(Self.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))) + } + } + + @inline(__always) func get(taskId: Int) -> RemoteImageRequest? { + Self.requestQueue.sync { Self.requestByTaskId[taskId] } + } + + @inline(__always) func add(taskId: Int, request: RemoteImageRequest) -> Void { + Self.requestQueue.async(flags: .barrier) { + Self.requestByTaskId[taskId] = request + Self.taskIdByRequestId[request.id] = taskId + } + } + + @inline(__always) func remove(taskId: Int, requestId: Int64) -> Void { + Self.requestQueue.async(flags: .barrier) { + Self.taskIdByRequestId[requestId] = nil + Self.requestByTaskId[taskId] = nil + } + } + + @inline(__always) func cancel(requestId: Int64) -> Void { + guard let request: RemoteImageRequest = (Self.requestQueue.sync { + guard let taskId = Self.taskIdByRequestId[requestId] else { return nil } + return Self.requestByTaskId[taskId] + }) else { return } + request.isCancelled = true + request.task?.cancel() + } +} diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index da52dc95cf..48320afa3f 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -80,7 +80,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.4.1 + 2.5.2 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..e18af39e04 100644 --- a/mobile/ios/Runner/Sync/Messages.g.swift +++ b/mobile/ios/Runner/Sync/Messages.g.swift @@ -312,6 +312,39 @@ 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 { @@ -323,6 +356,8 @@ private class MessagesPigeonCodecReader: FlutterStandardReader { return SyncDelta.fromList(self.readValue() as! [Any?]) case 132: return HashResult.fromList(self.readValue() as! [Any?]) + case 133: + return CloudIdResult.fromList(self.readValue() as! [Any?]) default: return super.readValue(ofType: type) } @@ -343,6 +378,9 @@ private class MessagesPigeonCodecWriter: FlutterStandardWriter { } else if let value = value as? HashResult { super.writeByte(132) super.writeValue(value.toList()) + } else if let value = value as? CloudIdResult { + super.writeByte(133) + super.writeValue(value.toList()) } else { super.writeValue(value) } @@ -377,6 +415,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 +599,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..0650b47879 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 +211,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 +227,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 +241,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 +284,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 +301,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 +332,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 +377,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 +390,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/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/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 c991493dd9..7a8f9a51a4 100644 --- a/mobile/lib/constants/enums.dart +++ b/mobile/lib/constants/enums.dart @@ -10,6 +10,6 @@ enum ActionSource { timeline, viewer } enum ButtonPosition { bottomBar, kebabMenu, other } -enum CleanupStep { selectDate, filterOptions, scan, delete } +enum CleanupStep { selectDate, scan, delete } -enum AssetFilterType { all, photosOnly, videosOnly } +enum AssetKeepType { none, photosOnly, videosOnly } 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..310e30ea62 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -22,6 +22,7 @@ sealed class BaseAsset { final int? durationInSeconds; final bool isFavorite; final String? livePhotoVideoId; + final bool isEdited; const BaseAsset({ required this.name, @@ -34,6 +35,7 @@ sealed class BaseAsset { this.durationInSeconds, this.isFavorite = false, this.livePhotoVideoId, + required this.isEdited, }); bool get isImage => type == AssetType.image; @@ -71,6 +73,7 @@ sealed class BaseAsset { height: ${height ?? ""}, durationInSeconds: ${durationInSeconds ?? ""}, isFavorite: $isFavorite, + isEdited: $isEdited, }'''; } @@ -85,7 +88,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 +103,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..887dfd3834 100644 --- a/mobile/lib/domain/models/asset/local_asset.model.dart +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -3,6 +3,7 @@ part of 'base_asset.model.dart'; class LocalAsset extends BaseAsset { final String id; final String? remoteAssetId; + final String? cloudId; final int orientation; final DateTime? adjustmentTime; @@ -12,6 +13,7 @@ class LocalAsset extends BaseAsset { const LocalAsset({ required this.id, String? remoteId, + this.cloudId, required super.name, super.checksum, required super.type, @@ -26,6 +28,7 @@ class LocalAsset extends BaseAsset { this.adjustmentTime, this.latitude, this.longitude, + required super.isEdited, }) : remoteAssetId = remoteId; @override @@ -53,12 +56,14 @@ class LocalAsset extends BaseAsset { width: ${width ?? ""}, height: ${height ?? ""}, durationInSeconds: ${durationInSeconds ?? ""}, - remoteId: ${remoteId ?? ""} + 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,6 +74,7 @@ class LocalAsset extends BaseAsset { if (identical(this, other)) return true; return super == other && id == other.id && + cloudId == other.cloudId && orientation == other.orientation && adjustmentTime == other.adjustmentTime && latitude == other.latitude && @@ -88,6 +94,7 @@ class LocalAsset extends BaseAsset { LocalAsset copyWith({ String? id, String? remoteId, + String? cloudId, String? name, String? checksum, AssetType? type, @@ -101,10 +108,12 @@ class LocalAsset extends BaseAsset { 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, @@ -118,6 +127,7 @@ class LocalAsset extends BaseAsset { 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..fc9cebc80f 100644 --- a/mobile/lib/domain/models/events.model.dart +++ b/mobile/lib/domain/models/events.model.dart @@ -30,3 +30,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..f6bed7cf61 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -82,7 +82,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/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..9019db664d 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'; @@ -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..8be3c2f224 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 1194331a6d..e4a129d322 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,7 @@ import 'package:logging/logging.dart'; class LocalSyncService { final DriftLocalAlbumRepository _localAlbumRepository; + final DriftLocalAssetRepository _localAssetRepository; final NativeSyncApi _nativeSyncApi; final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final LocalFilesManagerRepository _localFilesManager; @@ -26,11 +28,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 +51,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 +73,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 +103,8 @@ class LocalSyncService { } await updateAlbum(dbAlbum, album); } + + await _mapIosCloudIds(newAssets); } await _nativeSyncApi.checkpointSync(); } catch (e, s) { @@ -130,9 +143,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 +218,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 +259,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 +297,7 @@ class LocalSyncService { } await _localAlbumRepository.upsert(updatedDeviceAlbum, toUpsert: assetsToUpsert, toDelete: assetsToDelete); + await _mapIosCloudIds(assetsToUpsert); return true; } catch (e, s) { @@ -285,6 +306,29 @@ class LocalSyncService { return true; } + 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) && @@ -392,5 +436,6 @@ extension PlatformToLocalAsset on PlatformAsset { adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true), latitude: latitude, longitude: longitude, + isEdited: false, ); } 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/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..af1c94ca71 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,12 +54,32 @@ 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 semVer = 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, semVer); + + 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); @@ -51,9 +87,56 @@ class SyncStreamService { _logger.info("Resetting sync state as requested by server"); await _syncApiRepository.streamChanges(_handleEvents); } + + 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 +201,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: @@ -243,6 +330,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 e866a965c4..61e114762c 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); @@ -82,8 +81,8 @@ class TimelineFactory { TimelineService fromAssetsWithBuckets(List assets, TimelineOrigin type) => TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type)); - TimelineService map(String userId, LatLngBounds bounds) => - TimelineService(_timelineRepository.map(userId, bounds, groupBy)); + TimelineService map(List userIds, TimelineMapOptions options) => + TimelineService(_timelineRepository.map(userIds, options, groupBy)); } class TimelineService { 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/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_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index d2455b744e..9d154a5013 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,6 +17,8 @@ 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()(); @@ -43,5 +46,7 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData { 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..088cfac97d 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart @@ -21,6 +21,7 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder = i0.Value checksum, i0.Value isFavorite, i0.Value orientation, + i0.Value iCloudId, i0.Value adjustmentTime, i0.Value latitude, i0.Value longitude, @@ -38,6 +39,7 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder = i0.Value checksum, i0.Value isFavorite, i0.Value orientation, + i0.Value iCloudId, i0.Value adjustmentTime, i0.Value latitude, i0.Value longitude, @@ -108,6 +110,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), @@ -188,6 +195,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), @@ -252,6 +264,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, @@ -315,6 +330,7 @@ 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(), @@ -330,6 +346,7 @@ class $$LocalAssetEntityTableTableManager checksum: checksum, isFavorite: isFavorite, orientation: orientation, + iCloudId: iCloudId, adjustmentTime: adjustmentTime, latitude: latitude, longitude: longitude, @@ -347,6 +364,7 @@ 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(), @@ -362,6 +380,7 @@ class $$LocalAssetEntityTableTableManager checksum: checksum, isFavorite: isFavorite, orientation: orientation, + iCloudId: iCloudId, adjustmentTime: adjustmentTime, latitude: latitude, longitude: longitude, @@ -532,6 +551,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 @@ -578,6 +608,7 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity checksum, isFavorite, orientation, + iCloudId, adjustmentTime, latitude, longitude, @@ -661,6 +692,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 +777,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'], @@ -781,6 +822,7 @@ 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; @@ -796,6 +838,7 @@ class LocalAssetEntityData extends i0.DataClass this.checksum, required this.isFavorite, required this.orientation, + this.iCloudId, this.adjustmentTime, this.latitude, this.longitude, @@ -826,6 +869,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); } @@ -857,6 +903,7 @@ 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']), @@ -879,6 +926,7 @@ 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), @@ -897,6 +945,7 @@ 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(), @@ -914,6 +963,7 @@ 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, @@ -939,6 +989,7 @@ 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, @@ -961,6 +1012,7 @@ 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') @@ -981,6 +1033,7 @@ class LocalAssetEntityData extends i0.DataClass checksum, isFavorite, orientation, + iCloudId, adjustmentTime, latitude, longitude, @@ -1000,6 +1053,7 @@ 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); @@ -1018,6 +1072,7 @@ 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; @@ -1033,6 +1088,7 @@ 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(), @@ -1049,6 +1105,7 @@ 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(), @@ -1067,6 +1124,7 @@ class LocalAssetEntityCompanion i0.Expression? checksum, i0.Expression? isFavorite, i0.Expression? orientation, + i0.Expression? iCloudId, i0.Expression? adjustmentTime, i0.Expression? latitude, i0.Expression? longitude, @@ -1083,6 +1141,7 @@ 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, @@ -1101,6 +1160,7 @@ class LocalAssetEntityCompanion i0.Value? checksum, i0.Value? isFavorite, i0.Value? orientation, + i0.Value? iCloudId, i0.Value? adjustmentTime, i0.Value? latitude, i0.Value? longitude, @@ -1117,6 +1177,7 @@ 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, @@ -1161,6 +1222,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); } @@ -1187,6 +1251,7 @@ class LocalAssetEntityCompanion ..write('checksum: $checksum, ') ..write('isFavorite: $isFavorite, ') ..write('orientation: $orientation, ') + ..write('iCloudId: $iCloudId, ') ..write('adjustmentTime: $adjustmentTime, ') ..write('latitude: $latitude, ') ..write('longitude: $longitude') @@ -1194,3 +1259,8 @@ class LocalAssetEntityCompanion .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..1db22b558e 100644 --- a/mobile/lib/infrastructure/entities/merged_asset.drift +++ b/mobile/lib/infrastructure/entities/merged_asset.drift @@ -21,7 +21,12 @@ 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 FROM remote_asset_entity rae LEFT JOIN @@ -53,7 +58,12 @@ 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 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..f71aa8eb54 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 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 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,11 @@ 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'), ), ); } @@ -129,6 +134,11 @@ 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; MergedAssetResult({ this.remoteId, this.localId, @@ -146,6 +156,11 @@ class MergedAssetResult { this.livePhotoVideoId, required this.orientation, this.stackId, + this.iCloudId, + this.latitude, + this.longitude, + this.adjustmentTime, + required this.isEdited, }); } diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart index dcc885a2a9..4dc0fa568f 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -44,6 +44,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 +68,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..2d9e8b235e 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(); } 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/trashed_local_asset.entity.dart b/mobile/lib/infrastructure/entities/trashed_local_asset.entity.dart index 308130b9ea..d239588529 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,8 @@ class TrashedLocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntity IntColumn get orientation => integer().withDefault(const Constant(0))(); + IntColumn get source => intEnum()(); + @override Set get primaryKey => {id, albumId}; } @@ -36,5 +45,6 @@ extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityD height: height, width: width, orientation: orientation, + 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..eeec2b3019 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,7 @@ typedef $$TrashedLocalAssetEntityTableCreateCompanionBuilder = i0.Value checksum, i0.Value isFavorite, i0.Value orientation, + required i3.TrashOrigin source, }); typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder = i1.TrashedLocalAssetEntityCompanion Function({ @@ -37,6 +38,7 @@ typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder = i0.Value checksum, i0.Value isFavorite, i0.Value orientation, + i0.Value source, }); class $$TrashedLocalAssetEntityTableFilterComposer @@ -109,6 +111,12 @@ class $$TrashedLocalAssetEntityTableFilterComposer column: $table.orientation, builder: (column) => i0.ColumnFilters(column), ); + + i0.ColumnWithTypeConverterFilters + get source => $composableBuilder( + column: $table.source, + builder: (column) => i0.ColumnWithTypeConverterFilters(column), + ); } class $$TrashedLocalAssetEntityTableOrderingComposer @@ -180,6 +188,11 @@ class $$TrashedLocalAssetEntityTableOrderingComposer column: $table.orientation, builder: (column) => i0.ColumnOrderings(column), ); + + i0.ColumnOrderings get source => $composableBuilder( + column: $table.source, + builder: (column) => i0.ColumnOrderings(column), + ); } class $$TrashedLocalAssetEntityTableAnnotationComposer @@ -233,6 +246,9 @@ class $$TrashedLocalAssetEntityTableAnnotationComposer column: $table.orientation, builder: (column) => column, ); + + i0.GeneratedColumnWithTypeConverter get source => + $composableBuilder(column: $table.source, builder: (column) => column); } class $$TrashedLocalAssetEntityTableTableManager @@ -293,6 +309,7 @@ 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(), }) => i1.TrashedLocalAssetEntityCompanion( name: name, type: type, @@ -306,6 +323,7 @@ class $$TrashedLocalAssetEntityTableTableManager checksum: checksum, isFavorite: isFavorite, orientation: orientation, + source: source, ), createCompanionCallback: ({ @@ -321,6 +339,7 @@ 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, }) => i1.TrashedLocalAssetEntityCompanion.insert( name: name, type: type, @@ -334,6 +353,7 @@ class $$TrashedLocalAssetEntityTableTableManager checksum: checksum, isFavorite: isFavorite, orientation: orientation, + source: source, ), withReferenceMapper: (p0) => p0 .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) @@ -519,6 +539,17 @@ 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 List get $columns => [ name, type, @@ -532,6 +563,7 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity checksum, isFavorite, orientation, + source, ]; @override String get aliasedName => _alias ?? actualTableName; @@ -682,6 +714,12 @@ 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'], + )!, + ), ); } @@ -692,6 +730,8 @@ 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); @override bool get withoutRowId => true; @override @@ -712,6 +752,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass final String? checksum; final bool isFavorite; final int orientation; + final i3.TrashOrigin source; const TrashedLocalAssetEntityData({ required this.name, required this.type, @@ -725,6 +766,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass this.checksum, required this.isFavorite, required this.orientation, + required this.source, }); @override Map toColumns(bool nullToAbsent) { @@ -753,6 +795,11 @@ 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), + ); + } return map; } @@ -776,6 +823,9 @@ 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']), + ), ); } @override @@ -796,6 +846,9 @@ 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), + ), }; } @@ -812,6 +865,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass i0.Value checksum = const i0.Value.absent(), bool? isFavorite, int? orientation, + i3.TrashOrigin? source, }) => i1.TrashedLocalAssetEntityData( name: name ?? this.name, type: type ?? this.type, @@ -827,6 +881,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass checksum: checksum.present ? checksum.value : this.checksum, isFavorite: isFavorite ?? this.isFavorite, orientation: orientation ?? this.orientation, + source: source ?? this.source, ); TrashedLocalAssetEntityData copyWithCompanion( i1.TrashedLocalAssetEntityCompanion data, @@ -850,6 +905,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass orientation: data.orientation.present ? data.orientation.value : this.orientation, + source: data.source.present ? data.source.value : this.source, ); } @@ -867,7 +923,8 @@ class TrashedLocalAssetEntityData extends i0.DataClass ..write('albumId: $albumId, ') ..write('checksum: $checksum, ') ..write('isFavorite: $isFavorite, ') - ..write('orientation: $orientation') + ..write('orientation: $orientation, ') + ..write('source: $source') ..write(')')) .toString(); } @@ -886,6 +943,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass checksum, isFavorite, orientation, + source, ); @override bool operator ==(Object other) => @@ -902,7 +960,8 @@ 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); } class TrashedLocalAssetEntityCompanion @@ -919,6 +978,7 @@ class TrashedLocalAssetEntityCompanion final i0.Value checksum; final i0.Value isFavorite; final i0.Value orientation; + final i0.Value source; const TrashedLocalAssetEntityCompanion({ this.name = const i0.Value.absent(), this.type = const i0.Value.absent(), @@ -932,6 +992,7 @@ 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(), }); TrashedLocalAssetEntityCompanion.insert({ required String name, @@ -946,10 +1007,12 @@ class TrashedLocalAssetEntityCompanion this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), this.orientation = const i0.Value.absent(), + required i3.TrashOrigin source, }) : 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 +1026,7 @@ class TrashedLocalAssetEntityCompanion i0.Expression? checksum, i0.Expression? isFavorite, i0.Expression? orientation, + i0.Expression? source, }) { return i0.RawValuesInsertable({ if (name != null) 'name': name, @@ -977,6 +1041,7 @@ class TrashedLocalAssetEntityCompanion if (checksum != null) 'checksum': checksum, if (isFavorite != null) 'is_favorite': isFavorite, if (orientation != null) 'orientation': orientation, + if (source != null) 'source': source, }); } @@ -993,6 +1058,7 @@ class TrashedLocalAssetEntityCompanion i0.Value? checksum, i0.Value? isFavorite, i0.Value? orientation, + i0.Value? source, }) { return i1.TrashedLocalAssetEntityCompanion( name: name ?? this.name, @@ -1007,6 +1073,7 @@ class TrashedLocalAssetEntityCompanion checksum: checksum ?? this.checksum, isFavorite: isFavorite ?? this.isFavorite, orientation: orientation ?? this.orientation, + source: source ?? this.source, ); } @@ -1051,6 +1118,11 @@ class TrashedLocalAssetEntityCompanion if (orientation.present) { map['orientation'] = i0.Variable(orientation.value); } + if (source.present) { + map['source'] = i0.Variable( + i1.$TrashedLocalAssetEntityTable.$convertersource.toSql(source.value), + ); + } return map; } @@ -1068,7 +1140,8 @@ class TrashedLocalAssetEntityCompanion ..write('albumId: $albumId, ') ..write('checksum: $checksum, ') ..write('isFavorite: $isFavorite, ') - ..write('orientation: $orientation') + ..write('orientation: $orientation, ') + ..write('source: $source') ..write(')')) .toString(); } diff --git a/mobile/lib/infrastructure/loaders/image_request.dart b/mobile/lib/infrastructure/loaders/image_request.dart index d839b8bdf6..5be7b57835 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'; @@ -37,27 +34,61 @@ abstract class ImageRequest { void _onCancelled(); - Future _fromPlatformImage(Map info) async { - final address = info['pointer']; - if (address == null) { - return null; - } - + Future _fromEncodedPlatformImage(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; + } + + 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 +100,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..c2e3165aad 100644 --- a/mobile/lib/infrastructure/loaders/local_image_request.dart +++ b/mobile/lib/infrastructure/loaders/local_image_request.dart @@ -16,20 +16,23 @@ 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, ); + 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 _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..2da70c3ae1 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,18 @@ 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); + 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 _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..2ced28b810 100644 --- a/mobile/lib/infrastructure/loaders/thumbhash_image_request.dart +++ b/mobile/lib/infrastructure/loaders/thumbhash_image_request.dart @@ -11,8 +11,8 @@ 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); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index b42aa31550..2d30e3a0b9 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 => 18; @override MigrationStrategy get migration => MigrationStrategy( @@ -190,6 +192,27 @@ 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); + }, ), ); diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index bd72da949c..c561eef0c6 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>(); @@ -97,6 +101,7 @@ abstract class $Drift extends i0.GeneratedDatabase { localAlbumEntity, localAlbumAssetEntity, i4.idxLocalAssetChecksum, + i4.idxLocalAssetCloudId, i2.idxRemoteAssetOwnerChecksum, i2.uQRemoteAssetsOwnerChecksum, i2.uQRemoteAssetsOwnerLibraryChecksum, @@ -107,6 +112,7 @@ abstract class $Drift extends i0.GeneratedDatabase { remoteExifEntity, remoteAlbumAssetEntity, remoteAlbumUserEntity, + remoteAssetCloudIdEntity, memoryEntity, memoryAssetEntity, personEntity, @@ -114,8 +120,9 @@ abstract class $Drift extends i0.GeneratedDatabase { storeEntity, trashedLocalAssetEntity, i11.idxLatLng, - i19.idxTrashedLocalAssetChecksum, - i19.idxTrashedLocalAssetAlbum, + i14.idxRemoteAssetCloudId, + i20.idxTrashedLocalAssetChecksum, + i20.idxTrashedLocalAssetAlbum, ]; @override i0.StreamQueryUpdateRules @@ -249,6 +256,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 +352,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..72601f249f 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.steps.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart @@ -5941,6 +5941,1922 @@ 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)', + ); +} + i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, @@ -5955,6 +7871,10 @@ 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, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -6023,6 +7943,26 @@ 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; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -6043,6 +7983,10 @@ 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, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, @@ -6058,5 +8002,9 @@ i1.OnUpgrade stepByStep({ from11To12: from11To12, from12To13: from12To13, from13To14: from13To14, + from14To15: from14To15, + from15To16: from15To16, + from16To17: from16To17, + from17To18: from17To18, ), ); diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index 9d4c9bc496..a59e200923 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; diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart index 8cbce084cd..9d7cbd831b 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:immich_mobile/constants/constants.dart'; @@ -9,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; @@ -128,11 +137,12 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { return result; } - Future> getRemovalCandidates( + Future getRemovalCandidates( String userId, DateTime cutoffDate, { - AssetFilterType filterType = AssetFilterType.all, + AssetKeepType keepMediaType = AssetKeepType.none, bool keepFavorites = true, + Set keepAlbumIds = const {}, }) async { final iosSharedAlbumAssets = _db.localAlbumAssetEntity.selectOnly() ..addColumns([_db.localAlbumAssetEntity.assetId]) @@ -147,6 +157,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { 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 = @@ -157,10 +168,19 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { // Exclude assets that are in iOS shared albums whereClause = whereClause & _db.localAssetEntity.id.isNotInQuery(iosSharedAlbumAssets); - if (filterType == AssetFilterType.photosOnly) { - whereClause = whereClause & _db.localAssetEntity.type.equalsValue(AssetType.image); - } else if (filterType == AssetFilterType.videosOnly) { + 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) { @@ -170,6 +190,37 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { query.where(whereClause); final rows = await query.get(); - return rows.map((row) => row.readTable(_db.localAssetEntity).toDto()).toList(); + 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/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/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..043a42b1a4 100644 --- a/mobile/lib/infrastructure/repositories/search_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/search_api.repository.dart @@ -31,6 +31,7 @@ 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(), @@ -54,6 +55,7 @@ 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(), 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..d13083d706 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -19,6 +19,10 @@ 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, { Function()? onReset, @@ -45,6 +49,7 @@ class SyncApiRepository { SyncRequestType.usersV1, SyncRequestType.assetsV1, SyncRequestType.assetExifsV1, + SyncRequestType.assetMetadataV1, SyncRequestType.partnersV1, SyncRequestType.partnerAssetsV1, SyncRequestType.partnerAssetExifsV1, @@ -148,6 +153,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, 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..26f89432a5 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) { diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 66ae47a0b5..b0548bdd28 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,11 @@ class DriftTimelineRepository extends DriftDatabaseRepository { isFavorite: row.isFavorite, durationInSeconds: row.durationInSeconds, orientation: row.orientation, + cloudId: row.iCloudId, + latitude: row.latitude, + longitude: row.longitude, + adjustmentTime: row.adjustmentTime, + isEdited: row.isEdited, ), ) .get(); @@ -461,15 +483,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) { @@ -490,14 +512,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)!; @@ -506,8 +540,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository { } Future> _getMapBucketAssets( - String userId, - LatLngBounds bounds, { + List userId, + TimelineMapOptions options, { required int offset, required int count, }) { @@ -520,13 +554,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(); } diff --git a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart index 498e4227b7..7e93713c46 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,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { durationInSeconds: Value(item.asset.durationInSeconds), isFavorite: Value(item.asset.isFavorite), orientation: Value(item.asset.orientation), + source: TrashOrigin.localSync, ); batch.insert<$TrashedLocalAssetEntityTable, TrashedLocalAssetEntityData>( @@ -124,7 +126,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { Future trashLocalAsset(Map> assetsByAlbums) async { if (assetsByAlbums.isEmpty) { - return; + return Future.value(); } final companions = []; @@ -147,6 +149,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { orientation: Value(asset.orientation), createdAt: Value(asset.createdAt), updatedAt: Value(asset.updatedAt), + source: const Value(TrashOrigin.remoteSync), ), ); } @@ -165,7 +168,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { Future applyRestoredAssets(List idList) async { if (idList.isEmpty) { - return; + return Future.value(); } final trashedAssets = []; @@ -205,6 +208,58 @@ 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), + 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 83bc840df1..60bb1cb9c3 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -19,6 +19,7 @@ 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/infrastructure/repositories/network.repository.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'; @@ -237,6 +238,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); diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 93322f5031..2d45913fcb 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; @@ -183,6 +218,7 @@ class SearchFilter { SearchLocationFilter location; SearchCameraFilter camera; SearchDateFilter date; + SearchRatingFilter rating; SearchDisplayFilters display; // Enum @@ -200,6 +236,7 @@ class SearchFilter { required this.camera, required this.date, required this.display, + required this.rating, required this.mediaType, }); @@ -220,6 +257,7 @@ class SearchFilter { display.isNotInAlbum == false && display.isArchive == false && display.isFavorite == false && + rating.rating == null && mediaType == AssetType.other; } @@ -235,6 +273,7 @@ class SearchFilter { SearchCameraFilter? camera, SearchDateFilter? date, SearchDisplayFilters? display, + SearchRatingFilter? rating, AssetType? mediaType, }) { return SearchFilter( @@ -249,13 +288,14 @@ class SearchFilter { camera: camera ?? this.camera, date: date ?? this.date, display: display ?? this.display, + rating: rating ?? this.rating, mediaType: mediaType ?? this.mediaType, ); } @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, camera: $camera, date: $date, display: $display, rating: $rating, mediaType: $mediaType, assetId: $assetId)'; } @override @@ -273,6 +313,7 @@ class SearchFilter { other.camera == camera && other.date == date && other.display == display && + other.rating == rating && other.mediaType == mediaType; } @@ -289,6 +330,7 @@ class SearchFilter { camera.hashCode ^ date.hashCode ^ display.hashCode ^ + rating.hashCode ^ mediaType.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..5d78acb0b8 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; 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/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/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index 47052ea436..440544f989 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.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( 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/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/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index a1d7e55f32..e8f5eb2ee2 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -92,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..c7d786626c 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -75,6 +75,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(); @@ -132,7 +134,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/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/places/places_collection.page.dart b/mobile/lib/pages/library/places/places_collection.page.dart index f376709316..d6511cb25b 100644 --- a/mobile/lib/pages/library/places/places_collection.page.dart +++ b/mobile/lib/pages/library/places/places_collection.page.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, ), ), 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..e366cf70f1 100644 --- a/mobile/lib/pages/search/map/map.page.dart +++ b/mobile/lib/pages/search/map/map.page.dart @@ -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/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..8b7c82f15d --- /dev/null +++ b/mobile/lib/platform/local_image_api.g.dart @@ -0,0 +1,137 @@ +// 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, + }) 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, + ]); + 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..61bed52411 100644 --- a/mobile/lib/platform/native_sync_api.g.dart +++ b/mobile/lib/platform/native_sync_api.g.dart @@ -270,6 +270,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 @@ -289,6 +328,9 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is HashResult) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is CloudIdResult) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -305,6 +347,8 @@ class _PigeonCodec extends StandardMessageCodec { return SyncDelta.decode(readValue(buffer)!); case 132: return HashResult.decode(readValue(buffer)!); + case 133: + return CloudIdResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -616,4 +660,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/remote_image_api.g.dart b/mobile/lib/platform/remote_image_api.g.dart new file mode 100644 index 0000000000..410db03ece --- /dev/null +++ b/mobile/lib/platform/remote_image_api.g.dart @@ -0,0 +1,129 @@ +// 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, + }) 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]); + 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/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart index a159c6c54a..fe2ab61a58 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 > 10; + + final scrollView = CustomScrollView( + controller: _scrollController, + 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)); + }, + ), + ], + ); + 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_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_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_place.page.dart b/mobile/lib/presentation/pages/drift_place.page.dart index d042f52673..10b9ca7ae4 100644 --- a/mobile/lib/presentation/pages/drift_place.page.dart +++ b/mobile/lib/presentation/pages/drift_place.page.dart @@ -167,7 +167,7 @@ class _PlaceTile extends StatelessWidget { child: SizedBox( width: 80, height: 80, - child: Thumbnail.remote(remoteId: place.$2, fit: BoxFit.cover), + child: Thumbnail.remote(remoteId: place.$2, fit: BoxFit.cover, thumbhash: ""), ), ), ); diff --git a/mobile/lib/presentation/pages/editing/drift_edit.page.dart b/mobile/lib/presentation/pages/editing/drift_edit.page.dart index f9903b6b94..7e49348e19 100644 --- a/mobile/lib/presentation/pages/editing/drift_edit.page.dart +++ b/mobile/lib/presentation/pages/editing/drift_edit.page.dart @@ -2,6 +2,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 +13,7 @@ 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/widgets/common/immich_toast.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; @@ -78,7 +79,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/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 58ca892f5f..16655e98f6 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -18,6 +18,7 @@ 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/routing/router.dart'; import 'package:immich_mobile/widgets/common/feature_check.dart'; @@ -30,6 +31,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 { @@ -48,6 +50,7 @@ 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, @@ -62,10 +65,15 @@ class DriftSearchPage extends HookConsumerWidget { final cameraCurrentFilterWidget = useState(null); final locationCurrentFilterWidget = useState(null); final mediaTypeCurrentFilterWidget = useState(null); + final ratingCurrentFilterWidget = useState(null); final displayOptionCurrentFilterWidget = useState(null); final isSearching = useState(false); + final isRatingEnabled = ref + .watch(userMetadataPreferencesProvider) + .maybeWhen(data: (prefs) => prefs?.ratingsEnabled ?? false, orElse: () => false); + SnackBar searchInfoSnackBar(String message) { return SnackBar( content: Text(message, style: context.textTheme.labelLarge), @@ -369,6 +377,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) { @@ -629,6 +666,14 @@ class DriftSearchPage extends HookConsumerWidget { label: 'search_filter_media_type'.t(context: context), currentFilter: mediaTypeCurrentFilterWidget.value, ), + if (isRatingEnabled) ...[ + 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/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..4db297d658 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -14,14 +14,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/album/album_sort_by_options.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/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/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'; @@ -137,6 +138,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 +153,10 @@ class _AlbumSelectorState extends ConsumerState { Future filterAlbums() async { if (filter.query == null) { + if (!mounted) { + return; + } + setState(() { shownAlbums = sortedAlbums; }); @@ -159,6 +168,10 @@ class _AlbumSelectorState extends ConsumerState { .read(remoteAlbumProvider.notifier) .searchAlbums(sortedAlbums, filter.query!, filter.userId, filter.mode); + if (!mounted) { + return; + } + setState(() { shownAlbums = filteredAlbums; }); @@ -310,18 +323,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), @@ -344,15 +356,12 @@ class _SortButtonState extends ConsumerState<_SortButton> { Padding( padding: const EdgeInsets.only(right: 5), child: albumSortIsReverse - ? const Icon(Icons.keyboard_arrow_down) - : const Icon(Icons.keyboard_arrow_up_rounded), + ? 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 +551,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 +675,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 +695,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/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index 2a7ac9c7fe..2be2bdf765 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -92,7 +92,9 @@ class AssetViewer extends ConsumerStatefulWidget { if (asset.isVideo || asset.isMotionPhoto) { ref.read(videoPlaybackValueProvider.notifier).reset(); ref.read(videoPlayerControlsProvider.notifier).pause(); - // Hide controls by default for videos and motion photos + } + // Hide controls by default for videos + if (asset.isVideo) { ref.read(assetViewerProvider.notifier).setControls(false); } } @@ -118,7 +120,6 @@ class _AssetViewerState extends ConsumerState { bool dragInProgress = false; bool shouldPopOnDrag = false; bool assetReloadRequested = false; - double? initialScale; double previousExtent = _kBottomSheetMinimumExtent; Offset dragDownPosition = Offset.zero; int totalAssets = 0; @@ -148,6 +149,11 @@ class _AssetViewerState extends ConsumerState { if (asset != null) { _stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive(); } + if (ref.read(assetViewerProvider).showingControls) { + unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge)); + } else { + unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky)); + } } @override @@ -264,7 +270,6 @@ class _AssetViewerState extends ConsumerState { (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; } } @@ -316,7 +321,7 @@ class _AssetViewerState extends ConsumerState { hasDraggedDown = null; viewController?.animateMultiple( position: initialPhotoViewState.position, - scale: initialPhotoViewState.scale, + scale: viewController?.initialScale ?? initialPhotoViewState.scale, rotation: initialPhotoViewState.rotation, ); ref.read(assetViewerProvider.notifier).setOpacity(255); @@ -366,8 +371,9 @@ class _AssetViewerState extends ConsumerState { 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); + double? initialScale = viewController?.initialScale ?? initialPhotoViewState.scale; + if (initialScale != null) { + updatedScale = initialScale * (1.0 - scaleReduction); } final backgroundOpacity = (255 * (1.0 - (scaleReduction / dragRatio))).round(); @@ -481,8 +487,6 @@ class _AssetViewerState extends ConsumerState { 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, @@ -504,7 +508,7 @@ class _AssetViewerState extends ConsumerState { void _handleSheetClose() { viewController?.animateMultiple(position: Offset.zero); - viewController?.updateMultiple(scale: initialScale); + viewController?.updateMultiple(scale: viewController?.initialScale); ref.read(assetViewerProvider.notifier).setBottomSheet(false); sheetCloseController = null; shouldPopOnDrag = false; @@ -527,7 +531,9 @@ class _AssetViewerState extends ConsumerState { void _onScaleStateChanged(PhotoViewScaleState scaleState) { if (scaleState != PhotoViewScaleState.initial) { - ref.read(assetViewerProvider.notifier).setControls(false); + if (!dragInProgress) { + ref.read(assetViewerProvider.notifier).setControls(false); + } ref.read(videoPlayerControlsProvider.notifier).pause(); return; } @@ -611,6 +617,7 @@ class _AssetViewerState extends ConsumerState { filterQuality: FilterQuality.high, maxScale: 1.0, basePosition: Alignment.center, + disableScaleGestures: true, child: SizedBox( width: ctx.width, height: ctx.height, diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index ed3873b510..2e10e6856b 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -10,16 +10,19 @@ 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/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/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/rating_bar.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/infrastructure/user_metadata.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'; @@ -164,11 +167,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget { 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, - ), + title: 'appears_in'.t(context: context), + titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), Padding( padding: const EdgeInsets.only(left: 24), @@ -206,6 +206,9 @@ class _AssetDetailBottomSheet extends ConsumerWidget { 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); + final isRatingEnabled = ref + .watch(userMetadataPreferencesProvider) + .maybeWhen(data: (prefs) => prefs?.ratingsEnabled ?? false, orElse: () => false); // Build file info tile based on asset type Widget buildFileInfoTile() { @@ -224,9 +227,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { color: context.textTheme.labelLarge?.color, ), subtitle: _getFileInfo(asset, exifInfo), - subtitleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - ), + subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ); }, ); @@ -241,9 +242,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { color: context.textTheme.labelLarge?.color, ), subtitle: _getFileInfo(asset, exifInfo), - subtitleStyle: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - ), + subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ); } } @@ -262,11 +261,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget { 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, - ), + title: 'details'.t(context: context), + titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), // File info buildFileInfoTile(), @@ -278,9 +274,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { 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), - ), + subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ], // Lens info @@ -291,15 +285,42 @@ class _AssetDetailBottomSheet extends ConsumerWidget { 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), + subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + ], + // Rating bar + if (isRatingEnabled) ...[ + 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); + }, + ), + ], ), ), ], // 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), + const SizedBox(height: 60), ], ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart index 4edd6855a8..ce561c4016 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart @@ -4,6 +4,7 @@ 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'; @@ -64,11 +65,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 +78,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 +89,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 +103,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/bottom_sheet/sheet_people_details.widget.dart index 64f22eca92..d62a964401 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart @@ -4,6 +4,7 @@ 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'; @@ -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( 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..538a9bde20 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -96,7 +96,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; } 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/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/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index e77803c206..6e60c59c7f 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:async/async.dart'; import 'package:flutter/widgets.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -53,14 +51,14 @@ mixin CancellableImageProviderMixin on CancellableImageProvide Stream loadRequest(ImageRequest request, ImageDecoderCallback decode) 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()); + PaintingBinding.instance.imageCache.evict(this); return; } yield image; @@ -112,14 +110,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 +133,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 ? RemoteThumbProvider(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..d7454c0c89 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'; @@ -85,7 +84,7 @@ class LocalFullImageProvider extends CancellableImageProvider with CancellableImageProviderMixin { - static final cacheManager = RemoteThumbnailCacheManager(); final String assetId; + final String thumbhash; - RemoteThumbProvider({required this.assetId}); + RemoteThumbProvider({required this.assetId, required this.thumbhash}); @override Future obtainKey(ImageConfiguration configuration) { @@ -37,9 +36,8 @@ class RemoteThumbProvider extends CancellableImageProvider Stream _codec(RemoteThumbProvider key, ImageDecoderCallback decode) { final request = this.request = RemoteImageRequest( - uri: getThumbnailUrlForRemoteId(key.assetId), + uri: getThumbnailUrlForRemoteId(key.assetId, thumbhash: key.thumbhash), headers: ApiService.getRequestHeaders(), - cacheManager: cacheManager, ); return loadRequest(request, decode); } @@ -48,22 +46,23 @@ class RemoteThumbProvider extends CancellableImageProvider bool operator ==(Object other) { if (identical(this, other)) return true; if (other is RemoteThumbProvider) { - return assetId == other.assetId; + return assetId == other.assetId && thumbhash == other.thumbhash; } return false; } @override - int get hashCode => assetId.hashCode; + int get hashCode => assetId.hashCode ^ thumbhash.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,7 +73,7 @@ class RemoteFullImageProvider extends CancellableImageProvider [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty('Asset Id', key.assetId), @@ -87,39 +86,40 @@ class RemoteFullImageProvider extends CancellableImageProvider assetId.hashCode; + int get hashCode => assetId.hashCode ^ thumbhash.hashCode; } diff --git a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart index 92b1bb2544..f878c214a9 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 = RemoteThumbProvider(assetId: remoteId, thumbhash: thumbhash), + thumbhashProvider = null; Thumbnail.fromAsset({ required BaseAsset? asset, 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/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/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/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 5d6a2f0f4d..37b3145eb4 100644 --- a/mobile/lib/providers/background_sync.provider.dart +++ b/mobile/lib/providers/background_sync.provider.dart @@ -28,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 ec427613f1..2f067fdf67 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,44 +178,40 @@ 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, ), - ) { - { - _statusSubscription = _uploadService.taskStatusStream.listen(_handleTaskStatusUpdate); - _progressSubscription = _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 @@ -235,120 +227,12 @@ class DriftBackupNotifier extends StateNotifier { } } - void _handleTaskStatusUpdate(TaskStatusUpdate update) { - if (!mounted) { - _logger.warning("Skip _handleTaskStatusUpdate: notifier disposed"); - return; - } - 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) { - if (!mounted) { - _logger.warning("Skip _handleTaskProgressUpdate: notifier disposed"); - return; - } - final taskId = update.task.taskId; - final filename = update.task.displayName; - final progress = update.progress; - final currentItem = state.uploadItems[taskId]; - if (currentItem != null) { - if (progress == kUploadStatusCanceled) { - _removeUploadItem(update.task.taskId); - return; - } - - state = state.copyWith( - uploadItems: { - ...state.uploadItems, - taskId: update.hasExpectedFileSize - ? currentItem.copyWith( - progress: progress, - fileSize: update.expectedFileSize, - networkSpeedAsString: update.networkSpeedAsString, - ) - : currentItem.copyWith(progress: progress), - }, - ); - - return; - } - - state = state.copyWith( - uploadItems: { - ...state.uploadItems, - taskId: DriftUploadStatus( - taskId: taskId, - filename: filename, - progress: progress, - fileSize: update.expectedFileSize, - networkSpeedAsString: update.networkSpeedAsString, - ), - }, - ); - } - Future getBackupStatus(String userId) async { if (!mounted) { _logger.warning("Skip getBackupStatus (pre-call): notifier disposed"); return; } - final counts = await _uploadService.getBackupCounts(userId); + final counts = await _foregroundUploadService.getBackupCounts(userId); if (!mounted) { _logger.warning("Skip getBackupStatus (post-call): notifier disposed"); return; @@ -374,47 +258,126 @@ class DriftBackupNotifier extends StateNotifier { state = state.copyWith(isSyncing: isSyncing); } - Future startBackup(String userId) { + Future startForegroundBackup(String userId) async { 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 { - if (!mounted) { - _logger.warning("Skip cancel (pre-call): notifier disposed"); - return; + void _handleICloudProgress(String localAssetId, double progress) { + state = state.copyWith(iCloudDownloadProgress: {...state.iCloudDownloadProgress, localAssetId: progress}); + + if (progress >= 1.0) { + Future.delayed(const Duration(milliseconds: 250), () { + final updatedProgress = Map.from(state.iCloudDownloadProgress); + updatedProgress.remove(localAssetId); + state = state.copyWith(iCloudDownloadProgress: updatedProgress); + }); } - dPrint(() => "Canceling backup tasks..."); - state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true, error: BackupError.none); + } - final activeTaskCount = await _uploadService.cancelBackup(); - if (!mounted) { - _logger.warning("Skip cancel (post-call): notifier disposed"); + void _handleForegroundBackupProgress(String localAssetId, String filename, int bytes, int totalBytes) { + if (state.cancelToken == null) { return; } - if (activeTaskCount > 0) { - dPrint(() => "$activeTaskCount tasks left, continuing to cancel..."); - await cancel(); + 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 { - dPrint(() => "All tasks canceled successfully."); - // Clear all upload items when cancellation is complete - state = state.copyWith(isCanceling: false, uploadItems: {}); + state = state.copyWith( + uploadItems: { + ...state.uploadItems, + localAssetId: DriftUploadStatus( + taskId: localAssetId, + filename: filename, + progress: progress, + fileSize: totalBytes, + networkSpeedAsString: networkSpeedAsString, + ), + }, + ); } } - Future handleBackupResume(String userId) async { + 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("Resuming backup tasks..."); state = state.copyWith(error: BackupError.none); - final tasks = await _uploadService.getActiveTasks(kBackupGroup); + final tasks = await _backgroundUploadService.getActiveTasks(kBackupGroup); if (!mounted) { _logger.warning("Skip handleBackupResume (post-call): notifier disposed"); return; @@ -422,20 +385,12 @@ class DriftBackupNotifier extends StateNotifier { _logger.info("Found ${tasks.length} tasks"); if (tasks.isEmpty) { - // Start a new backup queue - _logger.info("Start a new backup queue"); - return startBackup(userId); + _logger.info("Start backup with URLSession"); + return _backgroundUploadService.uploadBackupCandidates(userId); } _logger.info("Tasks to resume: ${tasks.length}"); - return _uploadService.resumeBackup(); - } - - @override - void dispose() { - _statusSubscription?.cancel(); - _progressSubscription?.cancel(); - super.dispose(); + return _backgroundUploadService.resume(); } } @@ -445,7 +400,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 index 5b3b152f34..4d0bdba301 100644 --- a/mobile/lib/providers/cleanup.provider.dart +++ b/mobile/lib/providers/cleanup.provider.dart @@ -1,65 +1,150 @@ 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 AssetFilterType filterType; + 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.filterType = AssetFilterType.all, + this.keepMediaType = AssetKeepType.none, this.keepFavorites = true, + this.keepAlbumIds = const {}, }); CleanupState copyWith({ DateTime? selectedDate, List? assetsToDelete, + int? totalBytes, bool? isScanning, bool? isDeleting, - AssetFilterType? filterType, + 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, - filterType: filterType ?? this.filterType, + 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); + 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) : super(const CleanupState()); + 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 setFilterType(AssetFilterType filterType) { - state = state.copyWith(filterType: filterType, assetsToDelete: []); + 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 { @@ -69,13 +154,15 @@ class CleanupNotifier extends StateNotifier { state = state.copyWith(isScanning: true); try { - final assets = await _cleanupService.getRemovalCandidates( + final result = await _cleanupService.getRemovalCandidates( _userId, state.selectedDate!, - filterType: state.filterType, + keepMediaType: state.keepMediaType, keepFavorites: state.keepFavorites, + keepAlbumIds: state.keepAlbumIds, ); - state = state.copyWith(assetsToDelete: assets, isScanning: false); + + state = state.copyWith(assetsToDelete: result.assets, totalBytes: result.totalBytes, isScanning: false); } catch (e) { state = state.copyWith(isScanning: false); rethrow; @@ -101,6 +188,7 @@ class CleanupNotifier extends StateNotifier { } void reset() { - state = const CleanupState(); + // 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/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index d4d850d8c1..924e9c558a 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'; @@ -13,10 +14,11 @@ import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asse 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); @@ -357,6 +359,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 { @@ -411,14 +429,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/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..60300e74df 100644 --- a/mobile/lib/providers/infrastructure/platform.provider.dart +++ b/mobile/lib/providers/infrastructure/platform.provider.dart @@ -4,7 +4,8 @@ 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/remote_image_api.g.dart'; final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi())); @@ -16,4 +17,6 @@ final nativeSyncApiProvider = Provider((_) => NativeSyncApi()); final connectivityApiProvider = Provider((_) => ConnectivityApi()); -final thumbnailApi = ThumbnailApi(); +final localImageApi = LocalImageApi(); + +final remoteImageApi = RemoteImageApi(); 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/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..fba4fa7294 100644 --- a/mobile/lib/providers/server_info.provider.dart +++ b/mobile/lib/providers/server_info.provider.dart @@ -15,7 +15,7 @@ class ServerInfoNotifier extends StateNotifier { : super( const ServerInfo( serverVersion: ServerVersion(major: 0, minor: 0, patch: 0), - latestVersion: ServerVersion(major: 0, minor: 0, patch: 0), + latestVersion: null, serverFeatures: ServerFeatures(map: true, trash: true, oauthEnabled: false, passwordLogin: true), serverConfig: ServerConfig( trashDays: 30, @@ -32,17 +32,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 +76,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); } 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/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..22fa3bdd07 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -112,17 +112,23 @@ class AssetMediaRepository { : 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 +138,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; } } diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart index 654be78fb4..3a3e50f370 100644 --- a/mobile/lib/repositories/file_media.repository.dart +++ b/mobile/lib/repositories/file_media.repository.dart @@ -25,6 +25,7 @@ class FileMediaRepository { type: AssetType.image, createdAt: entity.createDateTime, updatedAt: entity.modifiedDateTime, + 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/services/action.service.dart b/mobile/lib/services/action.service.dart index 4261613a19..13e491f321 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); @@ -242,4 +239,17 @@ class ActionService { Future> downloadAll(List assets) { return _downloadRepository.downloadAllAssets(assets); } + + 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/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index aa247682a7..4e740ebfe5 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -54,7 +54,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..4eece142d2 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,49 @@ 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) { + return; + } + + 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) { 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 +197,8 @@ class UploadService { return activeTasks.length; } - Future resumeBackup() { + /// Resume background backup processing + Future resume() { return _uploadRepository.start(); } @@ -265,46 +256,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 +283,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 +314,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 +349,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 +380,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 +399,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 +434,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 index 6a4318d209..86ccac8067 100644 --- a/mobile/lib/services/cleanup.service.dart +++ b/mobile/lib/services/cleanup.service.dart @@ -1,6 +1,5 @@ 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/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; @@ -15,17 +14,19 @@ class CleanupService { const CleanupService(this._localAssetRepository, this._assetMediaRepository); - Future> getRemovalCandidates( + Future getRemovalCandidates( String userId, DateTime cutoffDate, { - AssetFilterType filterType = AssetFilterType.all, + AssetKeepType keepMediaType = AssetKeepType.none, bool keepFavorites = true, + Set keepAlbumIds = const {}, }) { return _localAssetRepository.getRemovalCandidates( userId, cutoffDate, - filterType: filterType, + keepMediaType: keepMediaType, keepFavorites: keepFavorites, + keepAlbumIds: keepAlbumIds, ); } @@ -42,4 +43,18 @@ class CleanupService { return 0; } + + /// 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..0803cfcdf0 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -1,5 +1,7 @@ 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/remote_album.service.dart'; @@ -12,6 +14,7 @@ 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/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 +33,7 @@ final deepLinkServiceProvider = Provider( ref.watch(beta_asset_provider.assetServiceProvider), ref.watch(remoteAlbumServiceProvider), ref.watch(driftMemoryServiceProvider), + ref.watch(currentUserProvider), ), ); @@ -47,6 +51,8 @@ class DeepLinkService { final RemoteAlbumService _betaRemoteAlbumService; final DriftMemoryService _betaMemoryServiceProvider; + final UserDto? _currentUser; + const DeepLinkService( this._memoryService, this._assetService, @@ -57,6 +63,7 @@ class DeepLinkService { this._betaAssetService, this._betaRemoteAlbumService, this._betaMemoryServiceProvider, + this._currentUser, ); DeepLink _handleColdStart(PageRouteInfo route, bool isColdStart) { @@ -107,6 +114,8 @@ class DeepLinkService { } else if (albumRegex.hasMatch(path)) { final albumId = albumRegex.firstMatch(path)?.group(1) ?? ''; deepLinkRoute = await _buildAlbumDeepLink(albumId); + } else if (path == "/memory") { + deepLinkRoute = await _buildMemoryDeepLink(null); } // Deep link resolution failed, safely handle it based on the app state @@ -118,17 +127,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 _betaMemoryServiceProvider.getMemoryLane(_currentUser.id); + } else { + final memory = await _betaMemoryServiceProvider.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) { 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/theme/theme_data.dart b/mobile/lib/theme/theme_data.dart index 8e3773839c..a633a04d7f 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,7 +61,12 @@ 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))), @@ -147,9 +152,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/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index f5c7513d1b..25ca64e8c3 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'; @@ -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/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/migration.dart b/mobile/lib/utils/migration.dart index 30a9702b53..94ae69321f 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -28,6 +28,7 @@ import 'package:immich_mobile/utils/datetime_helpers.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; + // ignore: import_rule_photo_manager import 'package:photo_manager/photo_manager.dart'; 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/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/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/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/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..58c73a77b8 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'; @@ -87,6 +88,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, @@ -271,6 +280,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { const AppBarServerInfo(), if (Store.isBetaTimelineEnabled && isReadonlyModeEnabled) buildReadonlyMessage(), buildAppLogButton(), + buildFreeUpSpaceButton(), buildSettingButton(), buildSignOutButton(), buildFooter(), 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..a341d6395c 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 @@ -170,50 +170,52 @@ class AppBarServerInfo extends HookConsumerWidget { ), ], ), - 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), + if (serverInfoState.latestVersion != null) ...[ + 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, + ), ), - 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, + ], ), ), ), - ), - ], - ), + 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, + ), + ), + ), + ), + ], + ), + ], ], ), ), diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index dd985ebfe2..4278dfa29d 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -50,6 +50,10 @@ class ImmichSliverAppBar extends ConsumerWidget { 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, diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 71086fd803..2aa770f104 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -414,6 +414,7 @@ class LoginForm extends HookConsumerWidget { keyboardAction: TextInputAction.next, keyboardType: TextInputType.url, autofillHints: const [AutofillHints.url], + autoCorrect: false, onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(), ), ), 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/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..32d90a28d9 100644 --- a/mobile/lib/widgets/map/map_thumbnail.dart +++ b/mobile/lib/widgets/map/map_thumbnail.dart @@ -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, @@ -109,8 +111,13 @@ class MapThumbnail extends HookConsumerWidget { ), ValueListenableBuilder( valueListenable: position, - builder: (_, value, __) => value != null && assetMarkerRemoteId != null - ? PositionedAssetMarkerIcon(size: height / 2, point: value, assetRemoteId: assetMarkerRemoteId!) + builder: (_, value, __) => value != null && assetMarkerRemoteId != null && assetThumbhash != null + ? PositionedAssetMarkerIcon( + size: height / 2, + point: value, + assetRemoteId: assetMarkerRemoteId!, + assetThumbhash: assetThumbhash!, + ) : const SizedBox.shrink(), ), ], diff --git a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart index 0944f7ce3e..becef728da 100644 --- a/mobile/lib/widgets/map/positioned_asset_marker_icon.dart +++ b/mobile/lib/widgets/map/positioned_asset_marker_icon.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/utils/image_url_builder.dart'; class PositionedAssetMarkerIcon extends StatelessWidget { final Point point; final String assetRemoteId; + final String assetThumbhash; final double size; final int durationInMilliseconds; @@ -18,6 +19,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,7 +37,7 @@ 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)), ), ), ); @@ -43,14 +45,15 @@ class PositionedAssetMarkerIcon extends StatelessWidget { } class _AssetMarkerIcon extends StatelessWidget { - const _AssetMarkerIcon({required this.id, super.key}); + 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); - final cacheKey = getThumbnailCacheKeyForRemoteId(id); + final cacheKey = getThumbnailCacheKeyForRemoteId(id, thumbhash); return LayoutBuilder( builder: (context, constraints) { return Stack( 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_gesture_detector.dart b/mobile/lib/widgets/photo_view/src/core/photo_view_gesture_detector.dart index 7a5406c675..0d2f6fa457 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 @@ -203,9 +203,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..cd70745703 100644 --- a/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart +++ b/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart @@ -108,6 +108,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 +144,7 @@ class _ImageWrapperState extends State { _lastStack = null; _didLoadSynchronously = synchronousCall; + widget.controller.scaleBoundaries = scaleBoundaries; } synchronousCall && !_didLoadSynchronously ? setupCB() : setState(setupCB); @@ -204,14 +216,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, 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/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/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index aee28c9449..d6b516a078 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'; @@ -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..5d82630fc6 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,12 @@ 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/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,10 +20,13 @@ class LayoutSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "asset_list_layout_sub_title".tr()), + SettingGroupTitle( + title: "asset_list_layout_sub_title".t(context: context), + icon: Icons.view_module_outlined, + ), SettingsSwitchListTile( valueNotifier: useDynamicLayout, - title: "asset_list_layout_settings_dynamic_layout_title".tr(), + title: "asset_list_layout_settings_dynamic_layout_title".t(context: context), onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), SettingsSliderListTile( 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/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..ef571fb30a 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 @@ -13,9 +13,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 +28,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 @@ -112,48 +117,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 +198,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 +206,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 +263,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 +293,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 +321,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 +352,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/free_up_space_settings.dart b/mobile/lib/widgets/settings/free_up_space_settings.dart index 7acb04686b..ee7ee20b00 100644 --- a/mobile/lib/widgets/settings/free_up_space_settings.dart +++ b/mobile/lib/widgets/settings/free_up_space_settings.dart @@ -3,13 +3,17 @@ 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}); @@ -21,6 +25,26 @@ class FreeUpSpaceSettings extends ConsumerStatefulWidget { 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(); @@ -35,20 +59,16 @@ class _FreeUpSpaceSettingsState extends ConsumerState { } if (state.selectedDate != null) { - return CleanupStep.filterOptions; + return CleanupStep.scan; } return CleanupStep.selectDate; } - void _goToFiltersStep() { - ref.read(hapticFeedbackProvider.notifier).mediumImpact(); - setState(() => _currentStep = CleanupStep.filterOptions); - } - void _goToScanStep() { ref.read(hapticFeedbackProvider.notifier).mediumImpact(); setState(() => _currentStep = CleanupStep.scan); + _scanAssets(); } void _setPresetDate(int daysAgo) { @@ -83,9 +103,17 @@ class _FreeUpSpaceSettingsState extends ConsumerState { 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(); @@ -127,6 +155,11 @@ class _FreeUpSpaceSettingsState extends ConsumerState { context: context, builder: (ctx) => _DeleteSuccessDialog(deletedCount: deletedCount), ); + + if (mounted) { + context.router.popUntilRoot(); + } + return; } setState(() => _currentStep = CleanupStep.selectDate); @@ -137,11 +170,20 @@ class _FreeUpSpaceSettingsState extends ConsumerState { 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) { @@ -172,28 +214,38 @@ class _FreeUpSpaceSettingsState extends ConsumerState { } final step1State = hasDate ? StepState.complete : StepState.indexed; - final step2State = hasDate ? StepState.complete : StepState.disabled; - final step3State = hasAssets + final step2State = hasAssets ? StepState.complete : hasDate ? StepState.indexed : StepState.disabled; - final step4State = hasAssets ? StepState.indexed : StepState.disabled; + final step3State = hasAssets ? StepState.indexed : StepState.disabled; - String getFilterSubtitle() { + final hasKeepSettings = + state.keepFavorites || state.keepAlbumIds.isNotEmpty || state.keepMediaType != AssetKeepType.none; + + String getKeepSettingsSummary() { final parts = []; - switch (state.filterType) { - case AssetFilterType.all: - parts.add('all'.t(context: context)); - case AssetFilterType.photosOnly: - parts.add('photos_only'.t(context: context)); - case AssetFilterType.videosOnly: - parts.add('videos_only'.t(context: context)); + + 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('keep_favorites'.t(context: context)); + parts.add('favorites'.t(context: context)); } - return parts.join(' • '); + + 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( @@ -214,12 +266,129 @@ class _FreeUpSpaceSettingsState extends ConsumerState { 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.labelLarge?.copyWith(fontSize: 15), + 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(), @@ -256,7 +425,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { content: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('cutoff_date_description'.t(context: context), style: context.textTheme.labelLarge), + Text('cutoff_date_description'.t(context: context), style: subtitleStyle), const SizedBox(height: 16), GridView.count( shrinkWrap: true, @@ -315,7 +484,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { ), const SizedBox(height: 16), ElevatedButton.icon( - onPressed: hasDate ? () => _goToFiltersStep() : null, + 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)), @@ -326,11 +495,11 @@ class _FreeUpSpaceSettingsState extends ConsumerState { state: step1State, ), - // Step 2: Select Filter Options + // Step 2: Scan Assets Step( stepStyle: styleForState(step2State), title: Text( - 'filter_options'.t(context: context), + 'scan'.t(context: context), style: context.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: step2State == StepState.complete @@ -340,91 +509,20 @@ class _FreeUpSpaceSettingsState extends ConsumerState { : context.colorScheme.onSurface, ), ), - subtitle: hasDate - ? Text( - getFilterSubtitle(), - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.primary, - fontWeight: FontWeight.w500, - ), - ) - : null, - content: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text('cleanup_filter_description'.t(context: context), style: context.textTheme.labelLarge), - const SizedBox(height: 16), - SegmentedButton( - segments: [ - ButtonSegment( - value: AssetFilterType.all, - label: Text('all'.t(context: context)), - icon: const Icon(Icons.photo_library), - ), - ButtonSegment( - value: AssetFilterType.photosOnly, - label: Text('photos'.t(context: context)), - icon: const Icon(Icons.photo), - ), - ButtonSegment( - value: AssetFilterType.videosOnly, - label: Text('videos'.t(context: context)), - icon: const Icon(Icons.videocam), - ), - ], - selected: {state.filterType}, - onSelectionChanged: (selection) { - ref.read(cleanupProvider.notifier).setFilterType(selection.first); - setState(() => _hasScanned = false); - }, - ), - const SizedBox(height: 16), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: Text('keep_favorites'.t(context: context), style: context.textTheme.titleSmall), - subtitle: Text( - 'keep_favorites_description'.t(context: context), - style: context.textTheme.labelLarge, - ), - value: state.keepFavorites, - onChanged: (value) { - ref.read(cleanupProvider.notifier).setKeepFavorites(value); - setState(() => _hasScanned = false); - }, - ), - const SizedBox(height: 16), - ElevatedButton.icon( - onPressed: _goToScanStep, - icon: const Icon(Icons.arrow_forward), - label: Text('continue'.t(context: context)), - style: ElevatedButton.styleFrom(minimumSize: const Size(double.infinity, 48)), - ), - ], - ), - isActive: hasDate, - state: step2State, - ), - - // Step 3: Scan Assets - Step( - stepStyle: styleForState(step3State), - title: Text( - 'scan'.t(context: context), - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: step3State == StepState.complete - ? context.colorScheme.primary - : step3State == StepState.disabled - ? context.colorScheme.onSurface.withValues(alpha: 0.38) - : context.colorScheme.onSurface, - ), - ), subtitle: _hasScanned ? Text( - 'cleanup_found_assets'.t( - context: context, - args: {'count': state.assetsToDelete.length.toString()}, - ), + 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 @@ -435,10 +533,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { : null, content: Column( children: [ - Text( - 'cleanup_step3_description'.t(context: context), - style: context.textTheme.labelLarge?.copyWith(fontSize: 15), - ), + Text('cleanup_step3_description'.t(context: context), style: subtitleStyle), if (CurrentPlatform.isIOS) ...[ const SizedBox(height: 12), Container( @@ -502,17 +597,17 @@ class _FreeUpSpaceSettingsState extends ConsumerState { ], ), isActive: hasDate, - state: step3State, + state: step2State, ), - // Step 4: Delete Assets + // Step 3: Delete Assets Step( - stepStyle: styleForState(step4State, isDestructive: true), + stepStyle: styleForState(step3State, isDestructive: true), title: Text( 'move_to_device_trash'.t(context: context), style: context.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, - color: step4State == StepState.disabled + color: step3State == StepState.disabled ? context.colorScheme.onSurface.withValues(alpha: 0.38) : context.colorScheme.error, ), @@ -528,15 +623,20 @@ class _FreeUpSpaceSettingsState extends ConsumerState { border: Border.all(color: context.colorScheme.error.withValues(alpha: 0.3)), ), child: hasAssets - ? 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), + ? 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, ), @@ -572,10 +672,11 @@ class _FreeUpSpaceSettingsState extends ConsumerState { ], ), isActive: hasAssets, - state: step4State, + state: step3State, ), ], ), + const SizedBox(height: 60), ], ), ), @@ -700,3 +801,107 @@ class _DatePresetCard extends StatelessWidget { ); } } + +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..da5ecab684 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'; @@ -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/makefile b/mobile/makefile index 3b211bcd09..79b263c079 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -7,12 +7,14 @@ 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 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 diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 657e62bf6d..5ca810fe48 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.1 +- API version: 2.5.2 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen @@ -102,7 +102,9 @@ Class | Method | HTTP request | Description *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,6 +112,7 @@ Class | Method | HTTP request | Description *AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | Get asset statistics *AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | Get random assets *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video +*AssetsApi* | [**removeAssetEdits**](doc//AssetsApi.md#removeassetedits) | **DELETE** /assets/{id}/edits | Remove edits from an existing asset *AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | Run an asset job *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | Update an asset @@ -135,6 +138,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 @@ -163,6 +171,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 @@ -346,6 +356,13 @@ Class | Method | HTTP request | Description - [AssetCopyDto](doc//AssetCopyDto.md) - [AssetDeltaSyncDto](doc//AssetDeltaSyncDto.md) - [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md) + - [AssetEditAction](doc//AssetEditAction.md) + - [AssetEditActionCrop](doc//AssetEditActionCrop.md) + - [AssetEditActionListDto](doc//AssetEditActionListDto.md) + - [AssetEditActionListDtoEditsInner](doc//AssetEditActionListDtoEditsInner.md) + - [AssetEditActionMirror](doc//AssetEditActionMirror.md) + - [AssetEditActionRotate](doc//AssetEditActionRotate.md) + - [AssetEditsDto](doc//AssetEditsDto.md) - [AssetFaceCreateDto](doc//AssetFaceCreateDto.md) - [AssetFaceDeleteDto](doc//AssetFaceDeleteDto.md) - [AssetFaceResponseDto](doc//AssetFaceResponseDto.md) @@ -393,7 +410,11 @@ 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) - [DownloadArchiveInfo](doc//DownloadArchiveInfo.md) - [DownloadInfoDto](doc//DownloadInfoDto.md) - [DownloadResponse](doc//DownloadResponse.md) @@ -423,7 +444,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) @@ -437,6 +461,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) @@ -497,6 +523,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) @@ -536,6 +563,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) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 59f31d1392..90e426b547 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,13 @@ 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_crop.dart'; +part 'model/asset_edit_action_list_dto.dart'; +part 'model/asset_edit_action_list_dto_edits_inner.dart'; +part 'model/asset_edit_action_mirror.dart'; +part 'model/asset_edit_action_rotate.dart'; +part 'model/asset_edits_dto.dart'; part 'model/asset_face_create_dto.dart'; part 'model/asset_face_delete_dto.dart'; part 'model/asset_face_response_dto.dart'; @@ -142,7 +150,11 @@ 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_info.dart'; part 'model/download_info_dto.dart'; part 'model/download_response.dart'; @@ -172,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'; @@ -186,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'; @@ -246,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'; @@ -285,6 +303,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'; 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..713bcafee3 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 = only own, undefined = all 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 = only own, undefined = all 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 ac50d015ed..5fda01a594 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -185,8 +185,10 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): + /// Asset ID /// /// * [String] key (required): + /// Metadata key Future deleteAssetMetadataWithHttpInfo(String id, String key,) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/metadata/{key}' @@ -221,8 +223,10 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): + /// Asset ID /// /// * [String] key (required): + /// Metadata key Future deleteAssetMetadata(String id, String key,) async { final response = await deleteAssetMetadataWithHttpInfo(id, key,); if (response.statusCode >= HttpStatus.badRequest) { @@ -336,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); @@ -351,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)); } @@ -380,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)); } @@ -398,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): + /// + /// * [AssetEditActionListDto] assetEditActionListDto (required): + Future editAssetWithHttpInfo(String id, AssetEditActionListDto assetEditActionListDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/assets/{id}/edits' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = assetEditActionListDto; + + 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): + /// + /// * [AssetEditActionListDto] assetEditActionListDto (required): + Future editAsset(String id, AssetEditActionListDto assetEditActionListDto,) async { + final response = await editAssetWithHttpInfo(id, assetEditActionListDto,); + 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), 'AssetEditsDto',) as AssetEditsDto; + + } + return null; + } + /// Retrieve assets by device ID /// /// Get all asset of a device that are in the database, ID only. @@ -407,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}' @@ -440,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) { @@ -458,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), 'AssetEditsDto',) as AssetEditsDto; + + } + return null; + } + /// Retrieve an asset /// /// Retrieve detailed information about a specific asset. @@ -599,8 +732,10 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): + /// Asset ID /// /// * [String] key (required): + /// Metadata key Future getAssetMetadataByKeyWithHttpInfo(String id, String key,) async { // ignore: prefer_const_declarations final apiPath = r'/assets/{id}/metadata/{key}' @@ -635,8 +770,10 @@ class AssetsApi { /// Parameters: /// /// * [String] id (required): + /// Asset ID /// /// * [String] key (required): + /// Metadata key Future getAssetMetadataByKey(String id, String key,) async { final response = await getAssetMetadataByKeyWithHttpInfo(id, key,); if (response.statusCode >= HttpStatus.badRequest) { @@ -721,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'; @@ -767,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) { @@ -795,6 +938,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'; @@ -831,6 +975,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) { @@ -921,6 +1066,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. @@ -932,22 +1126,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' @@ -1024,22 +1225,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) { @@ -1344,14 +1552,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): + /// File modification date /// /// * [String] key: /// @@ -1361,18 +1574,25 @@ 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: + /// 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'; @@ -1471,14 +1691,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): + /// File modification date /// /// * [String] key: /// @@ -1488,18 +1713,25 @@ 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: + /// 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) { @@ -1517,7 +1749,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]. /// @@ -1525,12 +1757,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); @@ -1542,6 +1778,9 @@ class AssetsApi { final headerParams = {}; final formParams = {}; + if (edited != null) { + queryParams.addAll(_queryParams('', 'edited', edited)); + } if (key != null) { queryParams.addAll(_queryParams('', 'key', key)); } @@ -1568,19 +1807,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/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/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..1b8ed3d9e4 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 /// /// * [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 /// /// * [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 587a9640b4..7f11db76d3 100644 --- a/mobile/openapi/lib/api/shared_links_api.dart +++ b/mobile/openapi/lib/api/shared_links_api.dart @@ -160,8 +160,10 @@ class SharedLinksApi { /// Parameters: /// /// * [String] albumId: + /// 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'; @@ -201,8 +203,10 @@ class SharedLinksApi { /// Parameters: /// /// * [String] 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) { @@ -232,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'; @@ -283,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) { 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/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 6f68e44cef..7f5cd50ed4 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -238,6 +238,20 @@ class ApiClient { return AssetDeltaSyncDto.fromJson(value); case 'AssetDeltaSyncResponseDto': return AssetDeltaSyncResponseDto.fromJson(value); + case 'AssetEditAction': + return AssetEditActionTypeTransformer().decode(value); + case 'AssetEditActionCrop': + return AssetEditActionCrop.fromJson(value); + case 'AssetEditActionListDto': + return AssetEditActionListDto.fromJson(value); + case 'AssetEditActionListDtoEditsInner': + return AssetEditActionListDtoEditsInner.fromJson(value); + case 'AssetEditActionMirror': + return AssetEditActionMirror.fromJson(value); + case 'AssetEditActionRotate': + return AssetEditActionRotate.fromJson(value); + case 'AssetEditsDto': + return AssetEditsDto.fromJson(value); case 'AssetFaceCreateDto': return AssetFaceCreateDto.fromJson(value); case 'AssetFaceDeleteDto': @@ -332,8 +346,16 @@ 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 'DownloadArchiveInfo': return DownloadArchiveInfo.fromJson(value); case 'DownloadInfoDto': @@ -392,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': @@ -420,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': @@ -540,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': @@ -618,6 +652,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': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 1a5f703c78..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(); } @@ -109,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(); } @@ -154,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..c770265860 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 asset 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,6 +86,8 @@ class AssetBulkUpdateDto { /// num? longitude; + /// Rating + /// /// Minimum value: -1 /// Maximum value: 5 /// @@ -88,6 +98,7 @@ class AssetBulkUpdateDto { /// 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 +107,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 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_crop.dart b/mobile/openapi/lib/model/asset_edit_action_crop.dart new file mode 100644 index 0000000000..7672ed825b --- /dev/null +++ b/mobile/openapi/lib/model/asset_edit_action_crop.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 AssetEditActionCrop { + /// Returns a new [AssetEditActionCrop] instance. + AssetEditActionCrop({ + required this.action, + required this.parameters, + }); + + /// Type of edit action to perform + AssetEditAction action; + + CropParameters parameters; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionCrop && + other.action == action && + other.parameters == parameters; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (action.hashCode) + + (parameters.hashCode); + + @override + String toString() => 'AssetEditActionCrop[action=$action, parameters=$parameters]'; + + Map toJson() { + final json = {}; + json[r'action'] = this.action; + json[r'parameters'] = this.parameters; + return json; + } + + /// Returns a new [AssetEditActionCrop] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetEditActionCrop? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionCrop"); + if (value is Map) { + final json = value.cast(); + + return AssetEditActionCrop( + action: AssetEditAction.fromJson(json[r'action'])!, + parameters: CropParameters.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 = AssetEditActionCrop.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 = AssetEditActionCrop.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetEditActionCrop-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] = AssetEditActionCrop.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_list_dto.dart b/mobile/openapi/lib/model/asset_edit_action_list_dto.dart new file mode 100644 index 0000000000..e843c66e8f --- /dev/null +++ b/mobile/openapi/lib/model/asset_edit_action_list_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 AssetEditActionListDto { + /// Returns a new [AssetEditActionListDto] instance. + AssetEditActionListDto({ + 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 AssetEditActionListDto && + _deepEquality.equals(other.edits, edits); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (edits.hashCode); + + @override + String toString() => 'AssetEditActionListDto[edits=$edits]'; + + Map toJson() { + final json = {}; + json[r'edits'] = this.edits; + return json; + } + + /// Returns a new [AssetEditActionListDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetEditActionListDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionListDto"); + if (value is Map) { + final json = value.cast(); + + return AssetEditActionListDto( + edits: AssetEditActionListDtoEditsInner.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 = AssetEditActionListDto.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 = AssetEditActionListDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetEditActionListDto-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] = AssetEditActionListDto.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_edit_action_list_dto_edits_inner.dart b/mobile/openapi/lib/model/asset_edit_action_list_dto_edits_inner.dart new file mode 100644 index 0000000000..00c9be2381 --- /dev/null +++ b/mobile/openapi/lib/model/asset_edit_action_list_dto_edits_inner.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 AssetEditActionListDtoEditsInner { + /// Returns a new [AssetEditActionListDtoEditsInner] instance. + AssetEditActionListDtoEditsInner({ + required this.action, + required this.parameters, + }); + + /// Type of edit action to perform + AssetEditAction action; + + MirrorParameters parameters; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionListDtoEditsInner && + other.action == action && + other.parameters == parameters; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (action.hashCode) + + (parameters.hashCode); + + @override + String toString() => 'AssetEditActionListDtoEditsInner[action=$action, parameters=$parameters]'; + + Map toJson() { + final json = {}; + json[r'action'] = this.action; + json[r'parameters'] = this.parameters; + return json; + } + + /// Returns a new [AssetEditActionListDtoEditsInner] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetEditActionListDtoEditsInner? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionListDtoEditsInner"); + if (value is Map) { + final json = value.cast(); + + return AssetEditActionListDtoEditsInner( + action: AssetEditAction.fromJson(json[r'action'])!, + parameters: MirrorParameters.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 = AssetEditActionListDtoEditsInner.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 = AssetEditActionListDtoEditsInner.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetEditActionListDtoEditsInner-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] = AssetEditActionListDtoEditsInner.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_mirror.dart b/mobile/openapi/lib/model/asset_edit_action_mirror.dart new file mode 100644 index 0000000000..aef98fc1a8 --- /dev/null +++ b/mobile/openapi/lib/model/asset_edit_action_mirror.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 AssetEditActionMirror { + /// Returns a new [AssetEditActionMirror] instance. + AssetEditActionMirror({ + required this.action, + required this.parameters, + }); + + /// Type of edit action to perform + AssetEditAction action; + + MirrorParameters parameters; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionMirror && + other.action == action && + other.parameters == parameters; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (action.hashCode) + + (parameters.hashCode); + + @override + String toString() => 'AssetEditActionMirror[action=$action, parameters=$parameters]'; + + Map toJson() { + final json = {}; + json[r'action'] = this.action; + json[r'parameters'] = this.parameters; + return json; + } + + /// Returns a new [AssetEditActionMirror] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetEditActionMirror? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionMirror"); + if (value is Map) { + final json = value.cast(); + + return AssetEditActionMirror( + action: AssetEditAction.fromJson(json[r'action'])!, + parameters: MirrorParameters.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 = AssetEditActionMirror.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 = AssetEditActionMirror.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetEditActionMirror-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] = AssetEditActionMirror.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_rotate.dart b/mobile/openapi/lib/model/asset_edit_action_rotate.dart new file mode 100644 index 0000000000..302e6a0ce6 --- /dev/null +++ b/mobile/openapi/lib/model/asset_edit_action_rotate.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 AssetEditActionRotate { + /// Returns a new [AssetEditActionRotate] instance. + AssetEditActionRotate({ + required this.action, + required this.parameters, + }); + + /// Type of edit action to perform + AssetEditAction action; + + RotateParameters parameters; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditActionRotate && + other.action == action && + other.parameters == parameters; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (action.hashCode) + + (parameters.hashCode); + + @override + String toString() => 'AssetEditActionRotate[action=$action, parameters=$parameters]'; + + Map toJson() { + final json = {}; + json[r'action'] = this.action; + json[r'parameters'] = this.parameters; + return json; + } + + /// Returns a new [AssetEditActionRotate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetEditActionRotate? fromJson(dynamic value) { + upgradeDto(value, "AssetEditActionRotate"); + if (value is Map) { + final json = value.cast(); + + return AssetEditActionRotate( + action: AssetEditAction.fromJson(json[r'action'])!, + parameters: RotateParameters.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 = AssetEditActionRotate.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 = AssetEditActionRotate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetEditActionRotate-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] = AssetEditActionRotate.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_edits_dto.dart b/mobile/openapi/lib/model/asset_edits_dto.dart new file mode 100644 index 0000000000..3bfbce8594 --- /dev/null +++ b/mobile/openapi/lib/model/asset_edits_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 AssetEditsDto { + /// Returns a new [AssetEditsDto] instance. + AssetEditsDto({ + required this.assetId, + this.edits = const [], + }); + + /// Asset ID to apply edits to + String assetId; + + /// List of edit actions to apply (crop, rotate, or mirror) + List edits; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetEditsDto && + other.assetId == assetId && + _deepEquality.equals(other.edits, edits); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetId.hashCode) + + (edits.hashCode); + + @override + String toString() => 'AssetEditsDto[assetId=$assetId, edits=$edits]'; + + Map toJson() { + final json = {}; + json[r'assetId'] = this.assetId; + json[r'edits'] = this.edits; + return json; + } + + /// Returns a new [AssetEditsDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetEditsDto? fromJson(dynamic value) { + upgradeDto(value, "AssetEditsDto"); + if (value is Map) { + final json = value.cast(); + + return AssetEditsDto( + assetId: mapValueOfType(json, r'assetId')!, + edits: AssetEditActionListDtoEditsInner.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 = AssetEditsDto.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 = AssetEditsDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetEditsDto-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] = AssetEditsDto.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 index 23c34d7152..6376ebc531 100644 --- a/mobile/openapi/lib/model/asset_metadata_bulk_delete_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_bulk_delete_dto.dart @@ -16,6 +16,7 @@ class AssetMetadataBulkDeleteDto { this.items = const [], }); + /// Metadata items to delete List items; @override 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 index a3a111f9f7..90417b79e0 100644 --- a/mobile/openapi/lib/model/asset_metadata_bulk_delete_item_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_bulk_delete_item_dto.dart @@ -17,8 +17,10 @@ class AssetMetadataBulkDeleteItemDto { required this.key, }); + /// Asset ID String assetId; + /// Metadata key String key; @override diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart index 15c130930b..b79a693726 100644 --- a/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart @@ -19,12 +19,16 @@ class AssetMetadataBulkResponseDto { required this.value, }); + /// Asset ID String assetId; + /// Metadata key String key; + /// Last update date DateTime updatedAt; + /// Metadata value (object) Object value; @override diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_dto.dart index fe9d9ed251..a5e770b02a 100644 --- a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_dto.dart @@ -16,6 +16,7 @@ class AssetMetadataBulkUpsertDto { this.items = const [], }); + /// Metadata items to upsert List items; @override diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart index 25a219537e..caaf379b30 100644 --- a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart @@ -18,10 +18,13 @@ class AssetMetadataBulkUpsertItemDto { required this.value, }); + /// Asset ID String assetId; + /// Metadata key String key; + /// Metadata value (object) Object value; @override diff --git a/mobile/openapi/lib/model/asset_metadata_response_dto.dart b/mobile/openapi/lib/model/asset_metadata_response_dto.dart index cccf42ae87..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, }); + /// Metadata key String key; + /// Last update date DateTime updatedAt; + /// Metadata value (object) Object value; @override 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 3d247f8572..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, }); + /// Metadata key String key; + /// Metadata value (object) Object value; @override 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..5422ccf55f 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 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_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..7fd938b31a 100644 --- a/mobile/openapi/lib/model/memory_create_dto.dart +++ b/mobile/openapi/lib/model/memory_create_dto.dart @@ -21,10 +21,12 @@ class MemoryCreateDto { required this.type, }); + /// Asset IDs to associate with memory List assetIds; OnThisDayDto data; + /// 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 +35,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,6 +47,7 @@ class MemoryCreateDto { /// DateTime? seenAt; + /// Memory type MemoryType type; @override 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..4a7ca403ab 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,6 +256,8 @@ class MetadataSearchDto { /// String? previewPath; + /// Filter by rating + /// /// Minimum value: -1 /// Maximum value: 5 /// @@ -238,6 +268,8 @@ class MetadataSearchDto { /// num? rating; + /// Number of results to return + /// /// Minimum value: 1 /// Maximum value: 1000 /// @@ -248,10 +280,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 +295,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 +304,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 +313,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 +322,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 +331,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 +340,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 +349,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 +358,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 +367,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 +376,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 +385,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 +394,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 diff --git a/mobile/openapi/lib/model/mirror_axis.dart b/mobile/openapi/lib/model/mirror_axis.dart new file mode 100644 index 0000000000..4deeeb047c --- /dev/null +++ b/mobile/openapi/lib/model/mirror_axis.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +/// Axis to mirror along +class MirrorAxis { + /// Instantiate a new enum with the provided [value]. + const MirrorAxis._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const horizontal = MirrorAxis._(r'horizontal'); + static const vertical = MirrorAxis._(r'vertical'); + + /// List of all possible values in this [enum][MirrorAxis]. + static const values = [ + horizontal, + vertical, + ]; + + static MirrorAxis? fromJson(dynamic value) => MirrorAxisTypeTransformer().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 = MirrorAxis.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// 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 MirrorAxisTypeTransformer._(); + + String encode(MirrorAxis data) => data.value; + + /// 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] + /// 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. + MirrorAxis? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'horizontal': return MirrorAxis.horizontal; + case r'vertical': return MirrorAxis.vertical; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// 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 5ba54f6eb5..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; + /// Supported contexts List supportedContexts; + /// Action title String title; @override diff --git a/mobile/openapi/lib/model/plugin_context_type.dart b/mobile/openapi/lib/model/plugin_context_type.dart index 797d2c3d3b..6f4ac91fdb 100644 --- a/mobile/openapi/lib/model/plugin_context_type.dart +++ b/mobile/openapi/lib/model/plugin_context_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Context type class PluginContextType { /// Instantiate a new enum with the provided [value]. const PluginContextType._(this.value); diff --git a/mobile/openapi/lib/model/plugin_filter_response_dto.dart b/mobile/openapi/lib/model/plugin_filter_response_dto.dart index 5873d72f07..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; + /// Supported contexts List supportedContexts; + /// Filter title String title; @override 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 index a6ee1c6b69..16a9604bcd 100644 --- a/mobile/openapi/lib/model/plugin_trigger_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart @@ -17,8 +17,10 @@ class PluginTriggerResponseDto { required this.type, }); + /// Context type PluginContextType contextType; + /// Trigger type PluginTriggerType type; @override diff --git a/mobile/openapi/lib/model/plugin_trigger_type.dart b/mobile/openapi/lib/model/plugin_trigger_type.dart index 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..7e0fb0c5c2 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,8 +156,11 @@ class RandomSearchDto { /// String? ocr; + /// Filter by person IDs List personIds; + /// Filter by rating + /// /// Minimum value: -1 /// Maximum value: 5 /// @@ -152,6 +171,8 @@ class RandomSearchDto { /// num? rating; + /// Number of results to return + /// /// Minimum value: 1 /// Maximum value: 1000 /// @@ -162,10 +183,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 +198,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 +207,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 +216,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 +225,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 +234,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 +243,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 +252,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 +261,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 +270,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 +279,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 +288,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 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_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..7d43cea872 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,6 +199,8 @@ class SmartSearchDto { /// String? queryAssetId; + /// Filter by rating + /// /// Minimum value: -1 /// Maximum value: 5 /// @@ -187,6 +211,8 @@ class SmartSearchDto { /// num? rating; + /// Number of results to return + /// /// Minimum value: 1 /// Maximum value: 1000 /// @@ -197,10 +223,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 +238,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 +247,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 +256,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 +265,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 +274,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 +283,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 +292,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 +301,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 +310,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 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..fce2feb421 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,8 +161,11 @@ class StatisticsSearchDto { /// String? ocr; + /// Filter by person IDs List personIds; + /// Filter by rating + /// /// Minimum value: -1 /// Maximum value: 5 /// @@ -156,10 +176,13 @@ class StatisticsSearchDto { /// 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 +191,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 +200,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 +209,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 +218,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 +227,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 +236,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 +245,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 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_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_metadata_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_metadata_delete_v1.dart index cf67b68dd2..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,8 +17,10 @@ class SyncAssetMetadataDeleteV1 { required this.key, }); + /// Asset ID String assetId; + /// Key String key; @override diff --git a/mobile/openapi/lib/model/sync_asset_metadata_v1.dart b/mobile/openapi/lib/model/sync_asset_metadata_v1.dart index 4fa6ed84ed..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; + /// Key String key; + /// Value Object value; @override 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..d1e321f39b 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); 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..135af3c7bb 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); 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..42e8ec387f 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,6 +71,8 @@ class UpdateAssetDto { /// num? longitude; + /// Rating + /// /// Minimum value: -1 /// Maximum value: 5 /// @@ -75,6 +83,7 @@ class UpdateAssetDto { /// 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 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..320d318062 100644 --- a/mobile/openapi/lib/model/user_admin_create_dto.dart +++ b/mobile/openapi/lib/model/user_admin_create_dto.dart @@ -24,10 +24,13 @@ class UserAdminCreateDto { 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 +39,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 +51,15 @@ class UserAdminCreateDto { /// bool? notify; + /// User password String password; + /// 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 +68,7 @@ class UserAdminCreateDto { /// bool? shouldChangePassword; + /// Storage label String? storageLabel; @override 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 1ad36f300b..ae3e6510aa 100644 --- a/mobile/openapi/lib/model/workflow_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_response_dto.dart @@ -24,22 +24,31 @@ 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; + /// Workflow trigger type PluginTriggerType triggerType; @override diff --git a/mobile/openapi/lib/model/workflow_update_dto.dart b/mobile/openapi/lib/model/workflow_update_dto.dart index 135c032b77..9891fff079 100644 --- a/mobile/openapi/lib/model/workflow_update_dto.dart +++ b/mobile/openapi/lib/model/workflow_update_dto.dart @@ -21,8 +21,10 @@ class WorkflowUpdateDto { 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 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 @@ -39,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 @@ -49,6 +54,7 @@ class WorkflowUpdateDto { /// String? name; + /// Workflow trigger type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/packages/ui/lib/src/components/text_input.dart b/mobile/packages/ui/lib/src/components/text_input.dart index f335df49f4..1b3fb91f51 100644 --- a/mobile/packages/ui/lib/src/components/text_input.dart +++ b/mobile/packages/ui/lib/src/components/text_input.dart @@ -12,6 +12,7 @@ class ImmichTextInput extends StatefulWidget { final List? autofillHints; final Widget? suffixIcon; final bool obscureText; + final bool autoCorrect; const ImmichTextInput({ super.key, @@ -26,6 +27,7 @@ class ImmichTextInput extends StatefulWidget { this.autofillHints, this.suffixIcon, this.obscureText = false, + this.autoCorrect = true, }); @override @@ -79,6 +81,7 @@ class _ImmichTextInputState extends State { validator: _validateInput, keyboardType: widget.keyboardType, textInputAction: widget.keyboardAction, + autocorrect: widget.autoCorrect, autofillHints: widget.autofillHints, onTap: () => setState(() => _error = null), onTapOutside: (_) => _focusNode.unfocus(), diff --git a/mobile/packages/ui/pubspec.lock b/mobile/packages/ui/pubspec.lock index b9d150f174..fa0b425230 100644 --- a/mobile/packages/ui/pubspec.lock +++ b/mobile/packages/ui/pubspec.lock @@ -34,10 +34,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" sky_engine: dependency: transitive description: flutter diff --git a/mobile/pigeon/thumbnail_api.dart b/mobile/pigeon/local_image_api.dart similarity index 71% rename from mobile/pigeon/thumbnail_api.dart rename to mobile/pigeon/local_image_api.dart index 0698e7cdc9..35b6734568 100644 --- a/mobile/pigeon/thumbnail_api.dart +++ b/mobile/pigeon/local_image_api.dart @@ -2,20 +2,20 @@ 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, @@ -23,7 +23,7 @@ abstract class ThumbnailApi { required bool isVideo, }); - 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..ae82018b02 100644 --- a/mobile/pigeon/native_sync_api.dart +++ b/mobile/pigeon/native_sync_api.dart @@ -90,6 +90,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 +129,7 @@ abstract class NativeSyncApi { @TaskQueue(type: TaskQueueType.serialBackgroundThread) Map> getTrashedAssets(); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getCloudIdForAssetIds(List assetIds); } diff --git a/mobile/pigeon/remote_image_api.dart b/mobile/pigeon/remote_image_api.dart new file mode 100644 index 0000000000..749deb828e --- /dev/null +++ b/mobile/pigeon/remote_image_api.dart @@ -0,0 +1,28 @@ +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, + }); + + void cancelRequest(int requestId); + + @async + int clearCache(); +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 3179d71bd1..d237c02023 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -337,6 +337,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 +377,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: @@ -936,6 +952,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 +1101,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: @@ -1270,6 +1302,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: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 7d484f0c64..198d3ad8f7 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.1+3030 +version: 2.5.2+3033 environment: sdk: '>=3.8.0 <4.0.0' @@ -86,6 +86,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 +129,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/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..17d02581d1 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, @@ -153,7 +157,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 +185,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())); }); diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index 109b54a907..0eabf3b612 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -19,12 +19,15 @@ import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/repositories/local_files_manager.repository.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 +53,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; @@ -82,6 +88,9 @@ void main() { mockStorageRepo = MockStorageRepository(); mockAbortCallbackWrapper = _MockAbortCallbackWrapper(); mockResetCallbackWrapper = _MockAbortCallbackWrapper(); + mockApi = MockApiService(); + mockServerApi = MockServerApi(); + mockSyncMigrationRepo = MockSyncMigrationRepository(); when(() => mockAbortCallbackWrapper()).thenReturn(false); @@ -94,6 +103,12 @@ void main() { }); 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 +142,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 +151,8 @@ void main() { trashedLocalAssetRepository: mockTrashedLocalAssetRepo, localFilesManager: mockLocalFilesManagerRepo, storageRepository: mockStorageRepo, + api: mockApi, + syncMigrationRepository: mockSyncMigrationRepo, ); when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((_) async => {}); @@ -216,6 +234,8 @@ void main() { localFilesManager: mockLocalFilesManagerRepo, storageRepository: mockStorageRepo, cancelChecker: cancellationChecker.call, + api: mockApi, + syncMigrationRepository: mockSyncMigrationRepo, ); await sut.sync(); @@ -255,6 +275,8 @@ void main() { localFilesManager: mockLocalFilesManagerRepo, storageRepository: mockStorageRepo, cancelChecker: cancellationChecker.call, + api: mockApi, + syncMigrationRepository: mockSyncMigrationRepo, ); await sut.sync(); @@ -474,11 +496,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 +504,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..1fe0fff6ae 100644 --- a/mobile/test/drift/main/generated/schema.dart +++ b/mobile/test/drift/main/generated/schema.dart @@ -17,6 +17,10 @@ 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; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -50,10 +54,37 @@ 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); 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, + ]; } 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/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart index 8d92011999..f3d6ab42a8 100644 --- a/mobile/test/fixtures/asset.stub.dart +++ b/mobile/test/fixtures/asset.stub.dart @@ -64,6 +64,7 @@ abstract final class LocalAssetStub { type: AssetType.image, createdAt: DateTime(2025), updatedAt: DateTime(2025, 2), + isEdited: false, ); static final image2 = LocalAsset( @@ -72,5 +73,6 @@ abstract final class LocalAssetStub { type: AssetType.image, createdAt: DateTime(2000), updatedAt: DateTime(20021), + 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/local_asset_repository_test.dart b/mobile/test/infrastructure/repositories/local_asset_repository_test.dart index 0d686fbc09..245cc86a98 100644 --- a/mobile/test/infrastructure/repositories/local_asset_repository_test.dart +++ b/mobile/test/infrastructure/repositories/local_asset_repository_test.dart @@ -1,4 +1,4 @@ -import 'package:drift/drift.dart'; +import 'package:drift/drift.dart' hide isNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/constants/enums.dart'; @@ -8,11 +8,13 @@ import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.d 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_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/infrastructure/repositories/local_asset.repository.dart'; void main() { + final now = DateTime(2024, 1, 15); late Drift db; late DriftLocalAssetRepository repository; @@ -25,68 +27,98 @@ void main() { await db.close(); }); + Future insertLocalAsset({ + required String id, + String? checksum, + DateTime? createdAt, + AssetType type = AssetType.image, + bool isFavorite = false, + String? iCloudId, + DateTime? adjustmentTime, + double? latitude, + double? longitude, + }) async { + final created = createdAt ?? now; + await db + .into(db.localAssetEntity) + .insert( + LocalAssetEntityCompanion.insert( + id: id, + name: 'asset_$id.jpg', + checksum: Value(checksum), + type: type, + createdAt: Value(created), + updatedAt: Value(created), + isFavorite: Value(isFavorite), + iCloudId: Value(iCloudId), + adjustmentTime: Value(adjustmentTime), + latitude: Value(latitude), + longitude: Value(longitude), + ), + ); + } + + Future insertRemoteAsset({ + required String id, + required String checksum, + required String ownerId, + DateTime? deletedAt, + }) async { + await db + .into(db.remoteAssetEntity) + .insert( + RemoteAssetEntityCompanion.insert( + id: id, + name: 'remote_$id.jpg', + checksum: checksum, + type: AssetType.image, + createdAt: Value(now), + updatedAt: Value(now), + ownerId: ownerId, + visibility: AssetVisibility.timeline, + deletedAt: Value(deletedAt), + ), + ); + } + + Future insertRemoteAssetCloudId({ + required String assetId, + required String? cloudId, + DateTime? createdAt, + DateTime? adjustmentTime, + double? latitude, + double? longitude, + }) async { + await db + .into(db.remoteAssetCloudIdEntity) + .insert( + RemoteAssetCloudIdEntityCompanion.insert( + assetId: assetId, + cloudId: Value(cloudId), + createdAt: Value(createdAt), + adjustmentTime: Value(adjustmentTime), + latitude: Value(latitude), + longitude: Value(longitude), + ), + ); + } + + Future insertUser(String id, String email) async { + await db.into(db.userEntity).insert(UserEntityCompanion.insert(id: id, email: email, name: email)); + } + group('getRemovalCandidates', () { final userId = 'user-123'; final otherUserId = 'user-456'; - final now = DateTime(2024, 1, 15); final cutoffDate = DateTime(2024, 1, 10); final beforeCutoff = DateTime(2024, 1, 5); final afterCutoff = DateTime(2024, 1, 12); - Future insertUser(String id, String email) async { - await db.into(db.userEntity).insert(UserEntityCompanion.insert(id: id, email: email, name: email)); - } - setUp(() async { await insertUser(userId, 'user@test.com'); await insertUser(otherUserId, 'other@test.com'); }); - Future insertLocalAsset({ - required String id, - required String checksum, - required DateTime createdAt, - required AssetType type, - required bool isFavorite, - }) async { - await db - .into(db.localAssetEntity) - .insert( - LocalAssetEntityCompanion.insert( - id: id, - name: 'asset_$id.jpg', - checksum: Value(checksum), - type: type, - createdAt: Value(createdAt), - updatedAt: Value(createdAt), - isFavorite: Value(isFavorite), - ), - ); - } - - Future insertRemoteAsset({ - required String id, - required String checksum, - required String ownerId, - DateTime? deletedAt, - }) async { - await db - .into(db.remoteAssetEntity) - .insert( - RemoteAssetEntityCompanion.insert( - id: id, - name: 'remote_$id.jpg', - checksum: checksum, - type: AssetType.image, - createdAt: Value(now), - updatedAt: Value(now), - ownerId: ownerId, - visibility: AssetVisibility.timeline, - deletedAt: Value(deletedAt), - ), - ); - } - Future insertLocalAlbum({required String id, required String name, required bool isIosSharedAlbum}) async { await db .into(db.localAlbumEntity) @@ -167,10 +199,10 @@ void main() { ); await insertRemoteAsset(id: 'remote-6', checksum: 'checksum-6', ownerId: userId); - final candidates = await repository.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); + final result = await repository.getRemovalCandidates(userId, cutoffDate, keepFavorites: true); - expect(candidates.length, 1); - expect(candidates[0].id, 'local-1'); + expect(result.assets.length, 1); + expect(result.assets[0].id, 'local-1'); }); test('includes favorites when keepFavorites is false', () async { @@ -183,14 +215,70 @@ void main() { ); await insertRemoteAsset(id: 'remote-favorite', checksum: 'checksum-fav', ownerId: userId); - final candidates = await repository.getRemovalCandidates(userId, cutoffDate, keepFavorites: false); + final result = await repository.getRemovalCandidates(userId, cutoffDate, keepFavorites: false); - expect(candidates.length, 1); - expect(candidates[0].id, 'local-favorite'); - expect(candidates[0].isFavorite, true); + expect(result.assets.length, 1); + expect(result.assets[0].id, 'local-favorite'); + expect(result.assets[0].isFavorite, true); }); - test('filters by photos only', () async { + test('keepMediaType photosOnly returns only videos for deletion', () async { + // Photo - should be kept + await insertLocalAsset( + id: 'local-photo', + checksum: 'checksum-photo', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-photo', checksum: 'checksum-photo', ownerId: userId); + + // Video - should be deleted + await insertLocalAsset( + id: 'local-video', + checksum: 'checksum-video', + createdAt: beforeCutoff, + type: AssetType.video, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); + + final result = await repository.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.photosOnly); + + expect(result.assets.length, 1); + expect(result.assets[0].id, 'local-video'); + expect(result.assets[0].type, AssetType.video); + }); + + test('keepMediaType videosOnly returns only photos for deletion', () async { + // Photo - should be deleted + await insertLocalAsset( + id: 'local-photo', + checksum: 'checksum-photo', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-photo', checksum: 'checksum-photo', ownerId: userId); + + // Video - should be kept + await insertLocalAsset( + id: 'local-video', + checksum: 'checksum-video', + createdAt: beforeCutoff, + type: AssetType.video, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); + + final result = await repository.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.videosOnly); + + expect(result.assets.length, 1); + expect(result.assets[0].id, 'local-photo'); + expect(result.assets[0].type, AssetType.image); + }); + + test('returns both photos and videos with keepMediaType.all', () async { // Photo await insertLocalAsset( id: 'local-photo', @@ -211,74 +299,10 @@ void main() { ); await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); - final candidates = await repository.getRemovalCandidates( - userId, - cutoffDate, - filterType: AssetFilterType.photosOnly, - ); + final result = await repository.getRemovalCandidates(userId, cutoffDate, keepMediaType: AssetKeepType.none); - expect(candidates.length, 1); - expect(candidates[0].id, 'local-photo'); - expect(candidates[0].type, AssetType.image); - }); - - test('filters by videos only', () async { - // Photo - await insertLocalAsset( - id: 'local-photo', - checksum: 'checksum-photo', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-photo', checksum: 'checksum-photo', ownerId: userId); - - // Video - await insertLocalAsset( - id: 'local-video', - checksum: 'checksum-video', - createdAt: beforeCutoff, - type: AssetType.video, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); - - final candidates = await repository.getRemovalCandidates( - userId, - cutoffDate, - filterType: AssetFilterType.videosOnly, - ); - - expect(candidates.length, 1); - expect(candidates[0].id, 'local-video'); - expect(candidates[0].type, AssetType.video); - }); - - test('returns both photos and videos with filterType.all', () async { - // Photo - await insertLocalAsset( - id: 'local-photo', - checksum: 'checksum-photo', - createdAt: beforeCutoff, - type: AssetType.image, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-photo', checksum: 'checksum-photo', ownerId: userId); - - // Video - await insertLocalAsset( - id: 'local-video', - checksum: 'checksum-video', - createdAt: beforeCutoff, - type: AssetType.video, - isFavorite: false, - ); - await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); - - final candidates = await repository.getRemovalCandidates(userId, cutoffDate, filterType: AssetFilterType.all); - - expect(candidates.length, 2); - final ids = candidates.map((a) => a.id).toSet(); + expect(result.assets.length, 2); + final ids = result.assets.map((a) => a.id).toSet(); expect(ids, containsAll(['local-photo', 'local-video'])); }); @@ -311,10 +335,10 @@ void main() { await insertRemoteAsset(id: 'remote-shared', checksum: 'checksum-shared', ownerId: userId); await insertLocalAlbumAsset(albumId: 'album-shared', assetId: 'local-shared'); - final candidates = await repository.getRemovalCandidates(userId, cutoffDate); + final result = await repository.getRemovalCandidates(userId, cutoffDate); - expect(candidates.length, 1); - expect(candidates[0].id, 'local-regular'); + expect(result.assets.length, 1); + expect(result.assets[0].id, 'local-regular'); }); test('includes assets at exact cutoff date', () async { @@ -327,10 +351,10 @@ void main() { ); await insertRemoteAsset(id: 'remote-exact', checksum: 'checksum-exact', ownerId: userId); - final candidates = await repository.getRemovalCandidates(userId, cutoffDate); + final result = await repository.getRemovalCandidates(userId, cutoffDate); - expect(candidates.length, 1); - expect(candidates[0].id, 'local-exact'); + expect(result.assets.length, 1); + expect(result.assets[0].id, 'local-exact'); }); test('returns empty list when no assets match criteria', () async { @@ -344,9 +368,9 @@ void main() { ); await insertRemoteAsset(id: 'remote-after', checksum: 'checksum-after', ownerId: userId); - final candidates = await repository.getRemovalCandidates(userId, cutoffDate); + final result = await repository.getRemovalCandidates(userId, cutoffDate); - expect(candidates, isEmpty); + expect(result.assets, isEmpty); }); test('handles multiple assets with same checksum', () async { @@ -367,10 +391,10 @@ void main() { ); await insertRemoteAsset(id: 'remote-dup', checksum: 'checksum-dup', ownerId: userId); - final candidates = await repository.getRemovalCandidates(userId, cutoffDate); + final result = await repository.getRemovalCandidates(userId, cutoffDate); - expect(candidates.length, 2); - expect(candidates.map((a) => a.checksum).toSet(), equals({'checksum-dup'})); + expect(result.assets.length, 2); + expect(result.assets.map((a) => a.checksum).toSet(), equals({'checksum-dup'})); }); test('includes assets not in any album', () async { @@ -384,10 +408,10 @@ void main() { ); await insertRemoteAsset(id: 'remote-no-album', checksum: 'checksum-no-album', ownerId: userId); - final candidates = await repository.getRemovalCandidates(userId, cutoffDate); + final result = await repository.getRemovalCandidates(userId, cutoffDate); - expect(candidates.length, 1); - expect(candidates[0].id, 'local-no-album'); + expect(result.assets.length, 1); + expect(result.assets[0].id, 'local-no-album'); }); test('excludes asset that is in both regular and iOS shared album', () async { @@ -409,9 +433,9 @@ void main() { await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-both'); await insertLocalAlbumAsset(albumId: 'album-shared', assetId: 'local-both'); - final candidates = await repository.getRemovalCandidates(userId, cutoffDate); + final result = await repository.getRemovalCandidates(userId, cutoffDate); - expect(candidates, isEmpty); + expect(result.assets, isEmpty); }); test('excludes assets with null checksum (not backed up)', () async { @@ -430,9 +454,523 @@ void main() { ), ); - final candidates = await repository.getRemovalCandidates(userId, cutoffDate); + final result = await repository.getRemovalCandidates(userId, cutoffDate); - expect(candidates, isEmpty); + expect(result.assets, isEmpty); + }); + + test('excludes assets in user-excluded albums', () async { + // Create two regular albums + await insertLocalAlbum(id: 'album-include', name: 'Include Album', isIosSharedAlbum: false); + await insertLocalAlbum(id: 'album-exclude', name: 'Exclude Album', isIosSharedAlbum: false); + + // Asset in included album - should be included + await insertLocalAsset( + id: 'local-in-included', + checksum: 'checksum-included', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-included', checksum: 'checksum-included', ownerId: userId); + await insertLocalAlbumAsset(albumId: 'album-include', assetId: 'local-in-included'); + + // Asset in excluded album - should NOT be included + await insertLocalAsset( + id: 'local-in-excluded', + checksum: 'checksum-excluded', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-excluded', checksum: 'checksum-excluded', ownerId: userId); + await insertLocalAlbumAsset(albumId: 'album-exclude', assetId: 'local-in-excluded'); + + final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-exclude'}); + + expect(result.assets.length, 1); + expect(result.assets[0].id, 'local-in-included'); + }); + + test('excludes assets that are in any of multiple excluded albums', () async { + // Create multiple albums + await insertLocalAlbum(id: 'album-1', name: 'Album 1', isIosSharedAlbum: false); + await insertLocalAlbum(id: 'album-2', name: 'Album 2', isIosSharedAlbum: false); + await insertLocalAlbum(id: 'album-3', name: 'Album 3', isIosSharedAlbum: false); + + // Asset in album-1 (excluded) - should NOT be included + await insertLocalAsset( + id: 'local-1', + checksum: 'checksum-1', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-1', checksum: 'checksum-1', ownerId: userId); + await insertLocalAlbumAsset(albumId: 'album-1', assetId: 'local-1'); + + // Asset in album-2 (excluded) - should NOT be included + await insertLocalAsset( + id: 'local-2', + checksum: 'checksum-2', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-2', checksum: 'checksum-2', ownerId: userId); + await insertLocalAlbumAsset(albumId: 'album-2', assetId: 'local-2'); + + // Asset in album-3 (not excluded) - should be included + await insertLocalAsset( + id: 'local-3', + checksum: 'checksum-3', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-3', checksum: 'checksum-3', ownerId: userId); + await insertLocalAlbumAsset(albumId: 'album-3', assetId: 'local-3'); + + final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-1', 'album-2'}); + + expect(result.assets.length, 1); + expect(result.assets[0].id, 'local-3'); + }); + + test('excludes asset that is in both excluded and non-excluded album', () async { + await insertLocalAlbum(id: 'album-included', name: 'Included Album', isIosSharedAlbum: false); + await insertLocalAlbum(id: 'album-excluded', name: 'Excluded Album', isIosSharedAlbum: false); + + // Asset in BOTH albums - should be excluded because it's in an excluded album + await insertLocalAsset( + id: 'local-both', + checksum: 'checksum-both', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-both', checksum: 'checksum-both', ownerId: userId); + await insertLocalAlbumAsset(albumId: 'album-included', assetId: 'local-both'); + await insertLocalAlbumAsset(albumId: 'album-excluded', assetId: 'local-both'); + + final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-excluded'}); + + expect(result.assets, isEmpty); + }); + + test('includes all assets when excludedAlbumIds is empty', () async { + await insertLocalAlbum(id: 'album-1', name: 'Album 1', isIosSharedAlbum: false); + + await insertLocalAsset( + id: 'local-1', + checksum: 'checksum-1', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-1', checksum: 'checksum-1', ownerId: userId); + await insertLocalAlbumAsset(albumId: 'album-1', assetId: 'local-1'); + + await insertLocalAsset( + id: 'local-2', + checksum: 'checksum-2', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-2', checksum: 'checksum-2', ownerId: userId); + + // Empty excludedAlbumIds should include all eligible assets + final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {}); + + expect(result.assets.length, 2); + }); + + test('excludes asset not in any album when album is excluded', () async { + await insertLocalAlbum(id: 'album-excluded', name: 'Excluded Album', isIosSharedAlbum: false); + + // Asset NOT in any album - should be included + await insertLocalAsset( + id: 'local-no-album', + checksum: 'checksum-no-album', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-no-album', checksum: 'checksum-no-album', ownerId: userId); + + // Asset in excluded album - should NOT be included + await insertLocalAsset( + id: 'local-in-excluded', + checksum: 'checksum-in-excluded', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-in-excluded', checksum: 'checksum-in-excluded', ownerId: userId); + await insertLocalAlbumAsset(albumId: 'album-excluded', assetId: 'local-in-excluded'); + + final result = await repository.getRemovalCandidates(userId, cutoffDate, keepAlbumIds: {'album-excluded'}); + + expect(result.assets.length, 1); + expect(result.assets[0].id, 'local-no-album'); + }); + + test('combines excludedAlbumIds with keepMediaType correctly', () async { + await insertLocalAlbum(id: 'album-excluded', name: 'Excluded Album', isIosSharedAlbum: false); + await insertLocalAlbum(id: 'album-regular', name: 'Regular Album', isIosSharedAlbum: false); + + // Photo in excluded album - should NOT be included (album excluded) + await insertLocalAsset( + id: 'local-photo-excluded', + checksum: 'checksum-photo-excluded', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-photo-excluded', checksum: 'checksum-photo-excluded', ownerId: userId); + await insertLocalAlbumAsset(albumId: 'album-excluded', assetId: 'local-photo-excluded'); + + // Video in regular album - should be included (keepMediaType photosOnly = delete videos) + await insertLocalAsset( + id: 'local-video', + checksum: 'checksum-video', + createdAt: beforeCutoff, + type: AssetType.video, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId); + await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-video'); + + // Photo in regular album - should NOT be included (keepMediaType photosOnly = keep photos) + await insertLocalAsset( + id: 'local-photo-regular', + checksum: 'checksum-photo-regular', + createdAt: beforeCutoff, + type: AssetType.image, + isFavorite: false, + ); + await insertRemoteAsset(id: 'remote-photo-regular', checksum: 'checksum-photo-regular', ownerId: userId); + await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-photo-regular'); + + final result = await repository.getRemovalCandidates( + userId, + cutoffDate, + keepMediaType: AssetKeepType.photosOnly, + keepAlbumIds: {'album-excluded'}, + ); + + expect(result.assets.length, 1); + expect(result.assets[0].id, 'local-video'); + }); + }); + + group('reconcileHashesFromCloudId', () { + final userId = 'user-123'; + final createdAt = DateTime(2024, 1, 10); + final adjustmentTime = DateTime(2024, 1, 11); + const latitude = 37.7749; + const longitude = -122.4194; + + setUp(() async { + await insertUser(userId, 'user@test.com'); + }); + + test('updates local asset checksum when all metadata matches', () async { + await insertLocalAsset( + id: 'local-1', + checksum: null, + iCloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); + + await insertRemoteAssetCloudId( + assetId: 'remote-1', + cloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await repository.reconcileHashesFromCloudId(); + + final updated = await repository.getById('local-1'); + expect(updated?.checksum, 'hash-abc123'); + }); + + test('does not update when local asset already has checksum', () async { + await insertLocalAsset( + id: 'local-1', + checksum: 'existing-checksum', + iCloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); + + await insertRemoteAssetCloudId( + assetId: 'remote-1', + cloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await repository.reconcileHashesFromCloudId(); + + final updated = await repository.getById('local-1'); + expect(updated?.checksum, 'existing-checksum'); + }); + + test('does not update when adjustment_time does not match', () async { + await insertLocalAsset( + id: 'local-1', + checksum: null, + iCloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); + + await insertRemoteAssetCloudId( + assetId: 'remote-1', + cloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: DateTime(2024, 1, 12), + latitude: latitude, + longitude: longitude, + ); + + await repository.reconcileHashesFromCloudId(); + + final updated = await repository.getById('local-1'); + expect(updated?.checksum, isNull); + }); + + test('does not update when latitude does not match', () async { + await insertLocalAsset( + id: 'local-1', + checksum: null, + iCloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); + + await insertRemoteAssetCloudId( + assetId: 'remote-1', + cloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: 40.7128, + longitude: longitude, + ); + + await repository.reconcileHashesFromCloudId(); + + final updated = await repository.getById('local-1'); + expect(updated?.checksum, isNull); + }); + + test('does not update when longitude does not match', () async { + await insertLocalAsset( + id: 'local-1', + checksum: null, + iCloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); + + await insertRemoteAssetCloudId( + assetId: 'remote-1', + cloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: -74.0060, + ); + + await repository.reconcileHashesFromCloudId(); + + final updated = await repository.getById('local-1'); + expect(updated?.checksum, isNull); + }); + + test('does not update when createdAt does not match', () async { + await insertLocalAsset( + id: 'local-1', + checksum: null, + iCloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); + + await insertRemoteAssetCloudId( + assetId: 'remote-1', + cloudId: 'cloud-123', + createdAt: DateTime(2024, 1, 5), + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await repository.reconcileHashesFromCloudId(); + + final updated = await repository.getById('local-1'); + expect(updated?.checksum, isNull); + }); + + test('does not update when iCloudId is null', () async { + await insertLocalAsset( + id: 'local-1', + checksum: null, + iCloudId: null, + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); + + await insertRemoteAssetCloudId( + assetId: 'remote-1', + cloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await repository.reconcileHashesFromCloudId(); + + final updated = await repository.getById('local-1'); + expect(updated?.checksum, isNull); + }); + + test('does not update when cloudId does not match iCloudId', () async { + await insertLocalAsset( + id: 'local-1', + checksum: null, + iCloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); + + await insertRemoteAssetCloudId( + assetId: 'remote-1', + cloudId: 'cloud-456', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await repository.reconcileHashesFromCloudId(); + + final updated = await repository.getById('local-1'); + expect(updated?.checksum, isNull); + }); + + test('handles partial null metadata fields matching correctly', () async { + await insertLocalAsset( + id: 'local-1', + checksum: null, + iCloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: null, + latitude: latitude, + longitude: longitude, + ); + + await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); + + await insertRemoteAssetCloudId( + assetId: 'remote-1', + cloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: null, + latitude: latitude, + longitude: longitude, + ); + + await repository.reconcileHashesFromCloudId(); + + final updated = await repository.getById('local-1'); + expect(updated?.checksum, 'hash-abc123'); + }); + + test('does not update when one has null and other has value', () async { + await insertLocalAsset( + id: 'local-1', + checksum: null, + iCloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: null, + longitude: longitude, + ); + + await insertRemoteAsset(id: 'remote-1', checksum: 'hash-abc123', ownerId: userId); + + await insertRemoteAssetCloudId( + assetId: 'remote-1', + cloudId: 'cloud-123', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await repository.reconcileHashesFromCloudId(); + + final updated = await repository.getById('local-1'); + expect(updated?.checksum, isNull); + }); + + test('handles no matching assets gracefully', () async { + await insertLocalAsset( + id: 'local-1', + checksum: null, + iCloudId: 'cloud-999', + createdAt: createdAt, + adjustmentTime: adjustmentTime, + latitude: latitude, + longitude: longitude, + ); + + await repository.reconcileHashesFromCloudId(); + + final updated = await repository.getById('local-1'); + expect(updated?.checksum, isNull); }); }); } 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/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/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/upload.service_test.dart b/mobile/test/services/background_upload.service_test.dart similarity index 50% rename from mobile/test/services/upload.service_test.dart rename to mobile/test/services/background_upload.service_test.dart index d33126782f..41dc46823d 100644 --- a/mobile/test/services/upload.service_test.dart +++ b/mobile/test/services/background_upload.service_test.dart @@ -1,30 +1,33 @@ +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/upload.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 '../repository.mocks.dart'; import '../mocks/asset_entity.mock.dart'; +import '../repository.mocks.dart'; void main() { - late UploadService sut; + late BackgroundUploadService sut; late MockUploadRepository mockUploadRepository; - late MockDriftBackupRepository mockBackupRepository; late MockStorageRepository mockStorageRepository; late MockDriftLocalAssetRepository mockLocalAssetRepository; + late MockDriftBackupRepository mockBackupRepository; late MockAppSettingsService mockAppSettingsService; late MockAssetMediaRepository mockAssetMediaRepository; late Drift db; @@ -46,20 +49,20 @@ void main() { setUp(() { mockUploadRepository = MockUploadRepository(); - mockBackupRepository = MockDriftBackupRepository(); 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 = UploadService( + sut = BackgroundUploadService( mockUploadRepository, - mockBackupRepository, mockStorageRepository, mockLocalAssetRepository, + mockBackupRepository, mockAppSettingsService, mockAssetMediaRepository, ); @@ -165,4 +168,184 @@ void main() { 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), + 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, + 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 + 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, + 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/test_utils.dart b/mobile/test/test_utils.dart index 498607e3d2..9d94e71052 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,7 @@ abstract final class TestUtils { width: width, height: height, orientation: orientation, + isEdited: false, ); } } diff --git a/mobile/test/test_utils/medium_factory.dart b/mobile/test/test_utils/medium_factory.dart index 19ad7166c6..b6f39ac3bd 100644 --- a/mobile/test/test_utils/medium_factory.dart +++ b/mobile/test/test_utils/medium_factory.dart @@ -27,6 +27,7 @@ class MediumFactory { type: type ?? AssetType.image, createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), + isEdited: false, ); } diff --git a/mobile/test/utils/action_button_utils_test.dart b/mobile/test/utils/action_button_utils_test.dart index e57b75c552..90795e8362 100644 --- a/mobile/test/utils/action_button_utils_test.dart +++ b/mobile/test/utils/action_button_utils_test.dart @@ -24,6 +24,7 @@ LocalAsset createLocalAsset({ createdAt: createdAt ?? DateTime.now(), updatedAt: updatedAt ?? DateTime.now(), isFavorite: isFavorite, + isEdited: false, ); } @@ -46,6 +47,7 @@ RemoteAsset createRemoteAsset({ createdAt: createdAt ?? DateTime.now(), updatedAt: updatedAt ?? DateTime.now(), isFavorite: isFavorite, + isEdited: false, ); } 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 70f482c5b2..7f85bbc1cf 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 = only own, undefined = all", "schema": { "type": "boolean" } @@ -1617,6 +1942,7 @@ "name": "withoutAssets", "required": false, "in": "query", + "description": "Exclude assets from response", "schema": { "type": "boolean" } @@ -2756,6 +3082,7 @@ "name": "deviceId", "required": true, "in": "path", + "description": "Device ID", "schema": { "type": "string" } @@ -2861,6 +3188,7 @@ "state": "Stable" } ], + "x-immich-permission": "asset.upload", "x-immich-state": "Stable" } }, @@ -2913,6 +3241,7 @@ "state": "Stable" } ], + "x-immich-permission": "job.create", "x-immich-state": "Stable" } }, @@ -3032,6 +3361,7 @@ "name": "count", "required": false, "in": "query", + "description": "Number of random assets to return", "schema": { "minimum": 1, "type": "number" @@ -3093,6 +3423,7 @@ "name": "isFavorite", "required": false, "in": "query", + "description": "Filter by favorite status", "schema": { "type": "boolean" } @@ -3101,6 +3432,7 @@ "name": "isTrashed", "required": false, "in": "query", + "description": "Filter by trash status", "schema": { "type": "boolean" } @@ -3109,6 +3441,7 @@ "name": "visibility", "required": false, "in": "query", + "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -3303,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/AssetEditsDto" + } + } + }, + "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/AssetEditActionListDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetEditsDto" + } + } + }, + "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.", @@ -3446,6 +3946,7 @@ "name": "id", "required": true, "in": "path", + "description": "Asset ID", "schema": { "format": "uuid", "type": "string" @@ -3455,6 +3956,7 @@ "name": "key", "required": true, "in": "path", + "description": "Metadata key", "schema": { "type": "string" } @@ -3505,6 +4007,7 @@ "name": "id", "required": true, "in": "path", + "description": "Asset ID", "schema": { "format": "uuid", "type": "string" @@ -3514,6 +4017,7 @@ "name": "key", "required": true, "in": "path", + "description": "Metadata key", "schema": { "type": "string" } @@ -3632,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, @@ -3789,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, @@ -3813,6 +4337,7 @@ "name": "size", "required": false, "in": "query", + "description": "Asset media size", "schema": { "$ref": "#/components/schemas/AssetMediaSize" } @@ -4807,6 +5332,7 @@ "name": "id", "required": true, "in": "query", + "description": "Face ID", "schema": { "format": "uuid", "type": "string" @@ -5162,6 +5688,7 @@ "name": "name", "required": true, "in": "path", + "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -5717,6 +6244,7 @@ "name": "fileCreatedAfter", "required": false, "in": "query", + "description": "Filter assets created after this date", "schema": { "format": "date-time", "type": "string" @@ -5726,6 +6254,7 @@ "name": "fileCreatedBefore", "required": false, "in": "query", + "description": "Filter assets created before this date", "schema": { "format": "date-time", "type": "string" @@ -5735,6 +6264,7 @@ "name": "isArchived", "required": false, "in": "query", + "description": "Filter by archived status", "schema": { "type": "boolean" } @@ -5743,6 +6273,7 @@ "name": "isFavorite", "required": false, "in": "query", + "description": "Filter by favorite status", "schema": { "type": "boolean" } @@ -5751,6 +6282,7 @@ "name": "withPartners", "required": false, "in": "query", + "description": "Include partner assets", "schema": { "type": "boolean" } @@ -5759,6 +6291,7 @@ "name": "withSharedAlbums", "required": false, "in": "query", + "description": "Include shared album assets", "schema": { "type": "boolean" } @@ -5808,6 +6341,7 @@ "state": "Stable" } ], + "x-immich-permission": "map.read", "x-immich-state": "Stable" } }, @@ -5820,6 +6354,7 @@ "name": "lat", "required": true, "in": "query", + "description": "Latitude (-90 to 90)", "schema": { "format": "double", "type": "number" @@ -5829,6 +6364,7 @@ "name": "lon", "required": true, "in": "query", + "description": "Longitude (-180 to 180)", "schema": { "format": "double", "type": "number" @@ -5879,6 +6415,7 @@ "state": "Stable" } ], + "x-immich-permission": "map.search", "x-immich-state": "Stable" } }, @@ -5891,6 +6428,7 @@ "name": "for", "required": false, "in": "query", + "description": "Filter by date", "schema": { "format": "date-time", "type": "string" @@ -5900,6 +6438,7 @@ "name": "isSaved", "required": false, "in": "query", + "description": "Filter by saved status", "schema": { "type": "boolean" } @@ -5908,6 +6447,7 @@ "name": "isTrashed", "required": false, "in": "query", + "description": "Include trashed memories", "schema": { "type": "boolean" } @@ -5916,6 +6456,7 @@ "name": "order", "required": false, "in": "query", + "description": "Sort order", "schema": { "$ref": "#/components/schemas/MemorySearchOrder" } @@ -5934,6 +6475,7 @@ "name": "type", "required": false, "in": "query", + "description": "Memory type", "schema": { "$ref": "#/components/schemas/MemoryType" } @@ -6054,6 +6596,7 @@ "name": "for", "required": false, "in": "query", + "description": "Filter by date", "schema": { "format": "date-time", "type": "string" @@ -6063,6 +6606,7 @@ "name": "isSaved", "required": false, "in": "query", + "description": "Filter by saved status", "schema": { "type": "boolean" } @@ -6071,6 +6615,7 @@ "name": "isTrashed", "required": false, "in": "query", + "description": "Include trashed memories", "schema": { "type": "boolean" } @@ -6079,6 +6624,7 @@ "name": "order", "required": false, "in": "query", + "description": "Sort order", "schema": { "$ref": "#/components/schemas/MemorySearchOrder" } @@ -6097,6 +6643,7 @@ "name": "type", "required": false, "in": "query", + "description": "Memory type", "schema": { "$ref": "#/components/schemas/MemoryType" } @@ -6530,6 +7077,7 @@ "name": "id", "required": false, "in": "query", + "description": "Filter by notification ID", "schema": { "format": "uuid", "type": "string" @@ -6539,6 +7087,7 @@ "name": "level", "required": false, "in": "query", + "description": "Filter by notification level", "schema": { "$ref": "#/components/schemas/NotificationLevel" } @@ -6547,6 +7096,7 @@ "name": "type", "required": false, "in": "query", + "description": "Filter by notification type", "schema": { "$ref": "#/components/schemas/NotificationType" } @@ -6555,6 +7105,7 @@ "name": "unread", "required": false, "in": "query", + "description": "Filter by unread status", "schema": { "type": "boolean" } @@ -7082,6 +7633,7 @@ "name": "direction", "required": true, "in": "query", + "description": "Partner direction", "schema": { "$ref": "#/components/schemas/PartnerDirection" } @@ -7431,6 +7983,7 @@ "name": "closestAssetId", "required": false, "in": "query", + "description": "Closest asset ID for similarity search", "schema": { "format": "uuid", "type": "string" @@ -7440,6 +7993,7 @@ "name": "closestPersonId", "required": false, "in": "query", + "description": "Closest person ID for similarity search", "schema": { "format": "uuid", "type": "string" @@ -7472,6 +8026,7 @@ "name": "withHidden", "required": false, "in": "query", + "description": "Include hidden people", "schema": { "type": "boolean" } @@ -8300,6 +8855,7 @@ "name": "name", "required": true, "in": "path", + "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -8354,6 +8910,7 @@ "name": "name", "required": true, "in": "path", + "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -8420,6 +8977,7 @@ "name": "name", "required": true, "in": "path", + "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -8477,6 +9035,7 @@ "name": "name", "required": true, "in": "path", + "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -8485,6 +9044,7 @@ "name": "status", "required": false, "in": "query", + "description": "Filter jobs by status", "schema": { "type": "array", "items": { @@ -8653,6 +9213,7 @@ "name": "albumIds", "required": false, "in": "query", + "description": "Filter by album IDs", "schema": { "type": "array", "items": { @@ -8665,6 +9226,7 @@ "name": "city", "required": false, "in": "query", + "description": "Filter by city name", "schema": { "nullable": true, "type": "string" @@ -8674,6 +9236,7 @@ "name": "country", "required": false, "in": "query", + "description": "Filter by country name", "schema": { "nullable": true, "type": "string" @@ -8683,6 +9246,7 @@ "name": "createdAfter", "required": false, "in": "query", + "description": "Filter by creation date (after)", "schema": { "format": "date-time", "type": "string" @@ -8692,6 +9256,7 @@ "name": "createdBefore", "required": false, "in": "query", + "description": "Filter by creation date (before)", "schema": { "format": "date-time", "type": "string" @@ -8701,6 +9266,7 @@ "name": "deviceId", "required": false, "in": "query", + "description": "Device ID to filter by", "schema": { "type": "string" } @@ -8709,6 +9275,7 @@ "name": "isEncoded", "required": false, "in": "query", + "description": "Filter by encoded status", "schema": { "type": "boolean" } @@ -8717,6 +9284,7 @@ "name": "isFavorite", "required": false, "in": "query", + "description": "Filter by favorite status", "schema": { "type": "boolean" } @@ -8725,6 +9293,7 @@ "name": "isMotion", "required": false, "in": "query", + "description": "Filter by motion photo status", "schema": { "type": "boolean" } @@ -8733,6 +9302,7 @@ "name": "isNotInAlbum", "required": false, "in": "query", + "description": "Filter assets not in any album", "schema": { "type": "boolean" } @@ -8741,6 +9311,7 @@ "name": "isOffline", "required": false, "in": "query", + "description": "Filter by offline status", "schema": { "type": "boolean" } @@ -8749,6 +9320,7 @@ "name": "lensModel", "required": false, "in": "query", + "description": "Filter by lens model", "schema": { "nullable": true, "type": "string" @@ -8758,6 +9330,7 @@ "name": "libraryId", "required": false, "in": "query", + "description": "Library ID to filter by", "schema": { "format": "uuid", "nullable": true, @@ -8768,6 +9341,7 @@ "name": "make", "required": false, "in": "query", + "description": "Filter by camera make", "schema": { "type": "string" } @@ -8776,6 +9350,7 @@ "name": "minFileSize", "required": false, "in": "query", + "description": "Minimum file size in bytes", "schema": { "minimum": 0, "type": "integer" @@ -8785,6 +9360,7 @@ "name": "model", "required": false, "in": "query", + "description": "Filter by camera model", "schema": { "nullable": true, "type": "string" @@ -8794,6 +9370,7 @@ "name": "ocr", "required": false, "in": "query", + "description": "Filter by OCR text content", "schema": { "type": "string" } @@ -8802,6 +9379,7 @@ "name": "personIds", "required": false, "in": "query", + "description": "Filter by person IDs", "schema": { "type": "array", "items": { @@ -8814,6 +9392,7 @@ "name": "rating", "required": false, "in": "query", + "description": "Filter by rating", "schema": { "minimum": -1, "maximum": 5, @@ -8824,6 +9403,7 @@ "name": "size", "required": false, "in": "query", + "description": "Number of results to return", "schema": { "minimum": 1, "maximum": 1000, @@ -8834,6 +9414,7 @@ "name": "state", "required": false, "in": "query", + "description": "Filter by state/province name", "schema": { "nullable": true, "type": "string" @@ -8843,6 +9424,7 @@ "name": "tagIds", "required": false, "in": "query", + "description": "Filter by tag IDs", "schema": { "nullable": true, "type": "array", @@ -8856,6 +9438,7 @@ "name": "takenAfter", "required": false, "in": "query", + "description": "Filter by taken date (after)", "schema": { "format": "date-time", "type": "string" @@ -8865,6 +9448,7 @@ "name": "takenBefore", "required": false, "in": "query", + "description": "Filter by taken date (before)", "schema": { "format": "date-time", "type": "string" @@ -8874,6 +9458,7 @@ "name": "trashedAfter", "required": false, "in": "query", + "description": "Filter by trash date (after)", "schema": { "format": "date-time", "type": "string" @@ -8883,6 +9468,7 @@ "name": "trashedBefore", "required": false, "in": "query", + "description": "Filter by trash date (before)", "schema": { "format": "date-time", "type": "string" @@ -8892,6 +9478,7 @@ "name": "type", "required": false, "in": "query", + "description": "Asset type filter", "schema": { "$ref": "#/components/schemas/AssetTypeEnum" } @@ -8900,6 +9487,7 @@ "name": "updatedAfter", "required": false, "in": "query", + "description": "Filter by update date (after)", "schema": { "format": "date-time", "type": "string" @@ -8909,6 +9497,7 @@ "name": "updatedBefore", "required": false, "in": "query", + "description": "Filter by update date (before)", "schema": { "format": "date-time", "type": "string" @@ -8918,6 +9507,7 @@ "name": "visibility", "required": false, "in": "query", + "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -8926,6 +9516,7 @@ "name": "withDeleted", "required": false, "in": "query", + "description": "Include deleted assets", "schema": { "type": "boolean" } @@ -8934,6 +9525,7 @@ "name": "withExif", "required": false, "in": "query", + "description": "Include EXIF data in response", "schema": { "type": "boolean" } @@ -9056,6 +9648,7 @@ "name": "name", "required": true, "in": "query", + "description": "Person name to search for", "schema": { "type": "string" } @@ -9064,6 +9657,7 @@ "name": "withHidden", "required": false, "in": "query", + "description": "Include hidden people", "schema": { "type": "boolean" } @@ -9126,6 +9720,7 @@ "name": "name", "required": true, "in": "query", + "description": "Place name to search for", "schema": { "type": "string" } @@ -9371,6 +9966,7 @@ "name": "country", "required": false, "in": "query", + "description": "Filter by country", "schema": { "type": "string" } @@ -9379,6 +9975,7 @@ "name": "includeNull", "required": false, "in": "query", + "description": "Include null values in suggestions", "x-immich-history": [ { "version": "v1.111.0", @@ -9398,6 +9995,7 @@ "name": "lensModel", "required": false, "in": "query", + "description": "Filter by lens model", "schema": { "type": "string" } @@ -9406,6 +10004,7 @@ "name": "make", "required": false, "in": "query", + "description": "Filter by camera make", "schema": { "type": "string" } @@ -9414,6 +10013,7 @@ "name": "model", "required": false, "in": "query", + "description": "Filter by camera model", "schema": { "type": "string" } @@ -9422,6 +10022,7 @@ "name": "state", "required": false, "in": "query", + "description": "Filter by state/province", "schema": { "type": "string" } @@ -9430,6 +10031,7 @@ "name": "type", "required": true, "in": "query", + "description": "Suggestion type", "schema": { "$ref": "#/components/schemas/SearchSuggestionType" } @@ -10493,6 +11095,7 @@ "name": "albumId", "required": false, "in": "query", + "description": "Filter by album ID", "schema": { "format": "uuid", "type": "string" @@ -10502,6 +11105,7 @@ "name": "id", "required": false, "in": "query", + "description": "Filter by shared link ID", "x-immich-history": [ { "version": "v2.5.0", @@ -10637,6 +11241,7 @@ "name": "password", "required": false, "in": "query", + "description": "Link password", "schema": { "example": "password", "type": "string" @@ -10654,6 +11259,7 @@ "name": "token", "required": false, "in": "query", + "description": "Access token", "schema": { "type": "string" } @@ -11116,6 +11722,7 @@ "name": "primaryAssetId", "required": false, "in": "query", + "description": "Filter by primary asset ID", "schema": { "format": "uuid", "type": "string" @@ -14119,6 +14726,7 @@ "state": "Stable" } ], + "x-immich-permission": "folder.read", "x-immich-state": "Stable" } }, @@ -14171,6 +14779,7 @@ "state": "Stable" } ], + "x-immich-permission": "folder.read", "x-immich-state": "Stable" } }, @@ -14448,7 +15057,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "2.4.1", + "version": "2.5.2", "contact": {} }, "tags": [ @@ -14476,6 +15085,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." @@ -14625,9 +15238,11 @@ "APIKeyCreateDto": { "properties": { "name": { + "description": "API key name", "type": "string" }, "permissions": { + "description": "List of permissions", "items": { "$ref": "#/components/schemas/Permission" }, @@ -14646,6 +15261,7 @@ "$ref": "#/components/schemas/APIKeyResponseDto" }, "secret": { + "description": "API key secret (only shown once)", "type": "string" } }, @@ -14658,22 +15274,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" } @@ -14690,9 +15311,11 @@ "APIKeyUpdateDto": { "properties": { "name": { + "description": "API key name", "type": "string" }, "permissions": { + "description": "List of permissions", "items": { "$ref": "#/components/schemas/Permission" }, @@ -14705,14 +15328,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": { @@ -14720,7 +15346,8 @@ { "$ref": "#/components/schemas/ReactionType" } - ] + ], + "description": "Activity type (like or comment)" } }, "required": [ @@ -14732,18 +15359,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": { @@ -14751,7 +15382,8 @@ { "$ref": "#/components/schemas/ReactionType" } - ] + ], + "description": "Activity type" }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -14769,9 +15401,11 @@ "ActivityStatisticsResponseDto": { "properties": { "comments": { + "description": "Number of comments", "type": "integer" }, "likes": { + "description": "Number of likes", "type": "integer" } }, @@ -14784,6 +15418,7 @@ "AddUsersDto": { "properties": { "albumUsers": { + "description": "Album users to add", "items": { "$ref": "#/components/schemas/AlbumUserAddDto" }, @@ -14799,6 +15434,7 @@ "AdminOnboardingUpdateDto": { "properties": { "isOnboarded": { + "description": "Is admin onboarded", "type": "boolean" } }, @@ -14810,9 +15446,11 @@ "AlbumResponseDto": { "properties": { "albumName": { + "description": "Album name", "type": "string" }, "albumThumbnailAssetId": { + "description": "Thumbnail asset ID", "nullable": true, "type": "string" }, @@ -14823,6 +15461,7 @@ "type": "array" }, "assetCount": { + "description": "Number of assets", "type": "integer" }, "assets": { @@ -14838,26 +15477,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" }, @@ -14866,22 +15512,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" } @@ -14907,12 +15558,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" } }, @@ -14931,9 +15585,11 @@ "$ref": "#/components/schemas/AlbumUserRole" } ], - "default": "editor" + "default": "editor", + "description": "Album user role" }, "userId": { + "description": "User ID", "format": "uuid", "type": "string" } @@ -14950,9 +15606,11 @@ { "$ref": "#/components/schemas/AlbumUserRole" } - ] + ], + "description": "Album user role" }, "userId": { + "description": "User ID", "format": "uuid", "type": "string" } @@ -14970,7 +15628,8 @@ { "$ref": "#/components/schemas/AlbumUserRole" } - ] + ], + "description": "Album user role" }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -14983,6 +15642,7 @@ "type": "object" }, "AlbumUserRole": { + "description": "Album user role", "enum": [ "editor", "viewer" @@ -14992,6 +15652,7 @@ "AlbumsAddAssetsDto": { "properties": { "albumIds": { + "description": "Album IDs", "items": { "format": "uuid", "type": "string" @@ -14999,6 +15660,7 @@ "type": "array" }, "assetIds": { + "description": "Asset IDs", "items": { "format": "uuid", "type": "string" @@ -15019,9 +15681,11 @@ { "$ref": "#/components/schemas/BulkIdErrorReason" } - ] + ], + "description": "Error reason" }, "success": { + "description": "Operation success", "type": "boolean" } }, @@ -15038,7 +15702,8 @@ "$ref": "#/components/schemas/AssetOrder" } ], - "default": "desc" + "default": "desc", + "description": "Default asset order for albums" } }, "required": [ @@ -15047,13 +15712,15 @@ "type": "object" }, "AlbumsUpdate": { + "description": "Album preferences", "properties": { "defaultAssetOrder": { "allOf": [ { "$ref": "#/components/schemas/AssetOrder" } - ] + ], + "description": "Default asset order for albums" } }, "type": "object" @@ -15061,9 +15728,11 @@ "AssetBulkDeleteDto": { "properties": { "force": { + "description": "Force delete even if in use", "type": "boolean" }, "ids": { + "description": "IDs to process", "items": { "format": "uuid", "type": "string" @@ -15079,19 +15748,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 asset ID", "nullable": true, "type": "string" }, "ids": { + "description": "Asset IDs to update", "items": { "format": "uuid", "type": "string" @@ -15099,20 +15773,25 @@ "type": "array" }, "isFavorite": { + "description": "Mark as favorite", "type": "boolean" }, "latitude": { + "description": "Latitude coordinate", "type": "number" }, "longitude": { + "description": "Longitude coordinate", "type": "number" }, "rating": { + "description": "Rating", "maximum": 5, "minimum": -1, "type": "number" }, "timeZone": { + "description": "Time zone (IANA timezone)", "type": "string" }, "visibility": { @@ -15120,7 +15799,8 @@ { "$ref": "#/components/schemas/AssetVisibility" } - ] + ], + "description": "Asset visibility" } }, "required": [ @@ -15131,6 +15811,7 @@ "AssetBulkUploadCheckDto": { "properties": { "assets": { + "description": "Assets to check", "items": { "$ref": "#/components/schemas/AssetBulkUploadCheckItem" }, @@ -15145,10 +15826,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" } }, @@ -15161,6 +15843,7 @@ "AssetBulkUploadCheckResponseDto": { "properties": { "results": { + "description": "Upload check results", "items": { "$ref": "#/components/schemas/AssetBulkUploadCheckResult" }, @@ -15175,6 +15858,7 @@ "AssetBulkUploadCheckResult": { "properties": { "action": { + "description": "Upload action", "enum": [ "accept", "reject" @@ -15182,15 +15866,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" @@ -15208,29 +15896,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" } @@ -15244,10 +15939,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" @@ -15264,15 +15961,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" }, @@ -15286,32 +15986,167 @@ ], "type": "object" }, + "AssetEditAction": { + "description": "Type of edit action to perform", + "enum": [ + "crop", + "rotate", + "mirror" + ], + "type": "string" + }, + "AssetEditActionCrop": { + "properties": { + "action": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetEditAction" + } + ], + "description": "Type of edit action to perform" + }, + "parameters": { + "$ref": "#/components/schemas/CropParameters" + } + }, + "required": [ + "action", + "parameters" + ], + "type": "object" + }, + "AssetEditActionListDto": { + "properties": { + "edits": { + "description": "List of edit actions to apply (crop, rotate, or mirror)", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/AssetEditActionCrop" + }, + { + "$ref": "#/components/schemas/AssetEditActionRotate" + }, + { + "$ref": "#/components/schemas/AssetEditActionMirror" + } + ] + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "edits" + ], + "type": "object" + }, + "AssetEditActionMirror": { + "properties": { + "action": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetEditAction" + } + ], + "description": "Type of edit action to perform" + }, + "parameters": { + "$ref": "#/components/schemas/MirrorParameters" + } + }, + "required": [ + "action", + "parameters" + ], + "type": "object" + }, + "AssetEditActionRotate": { + "properties": { + "action": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetEditAction" + } + ], + "description": "Type of edit action to perform" + }, + "parameters": { + "$ref": "#/components/schemas/RotateParameters" + } + }, + "required": [ + "action", + "parameters" + ], + "type": "object" + }, + "AssetEditsDto": { + "properties": { + "assetId": { + "description": "Asset ID to apply edits to", + "format": "uuid", + "type": "string" + }, + "edits": { + "description": "List of edit actions to apply (crop, rotate, or mirror)", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/AssetEditActionCrop" + }, + { + "$ref": "#/components/schemas/AssetEditActionRotate" + }, + { + "$ref": "#/components/schemas/AssetEditActionMirror" + } + ] + }, + "minItems": 1, + "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" } }, @@ -15330,6 +16165,7 @@ "AssetFaceDeleteDto": { "properties": { "force": { + "description": "Force delete even if person has other faces", "type": "boolean" } }, @@ -15341,25 +16177,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": { @@ -15368,6 +16211,7 @@ "$ref": "#/components/schemas/PersonResponseDto" } ], + "description": "Person associated with face", "nullable": true }, "sourceType": { @@ -15375,7 +16219,8 @@ { "$ref": "#/components/schemas/SourceType" } - ] + ], + "description": "Face detection source type" } }, "required": [ @@ -15393,6 +16238,7 @@ "AssetFaceUpdateDto": { "properties": { "data": { + "description": "Face update items", "items": { "$ref": "#/components/schemas/AssetFaceUpdateItem" }, @@ -15407,10 +16253,12 @@ "AssetFaceUpdateItem": { "properties": { "assetId": { + "description": "Asset ID", "format": "uuid", "type": "string" }, "personId": { + "description": "Person ID", "format": "uuid", "type": "string" } @@ -15424,25 +16272,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": { @@ -15450,7 +16305,8 @@ { "$ref": "#/components/schemas/SourceType" } - ] + ], + "description": "Face detection source type" } }, "required": [ @@ -15467,18 +16323,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" } @@ -15492,6 +16352,7 @@ "AssetIdsDto": { "properties": { "assetIds": { + "description": "Asset IDs", "items": { "format": "uuid", "type": "string" @@ -15507,9 +16368,11 @@ "AssetIdsResponseDto": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" }, "error": { + "description": "Error reason if failed", "enum": [ "duplicate", "no_permission", @@ -15518,6 +16381,7 @@ "type": "string" }, "success": { + "description": "Whether operation succeeded", "type": "boolean" } }, @@ -15528,6 +16392,7 @@ "type": "object" }, "AssetJobName": { + "description": "Job name", "enum": [ "refresh-faces", "refresh-metadata", @@ -15539,6 +16404,7 @@ "AssetJobsDto": { "properties": { "assetIds": { + "description": "Asset IDs", "items": { "format": "uuid", "type": "string" @@ -15550,7 +16416,8 @@ { "$ref": "#/components/schemas/AssetJobName" } - ] + ], + "description": "Job name" } }, "required": [ @@ -15562,43 +16429,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" }, @@ -15607,7 +16485,8 @@ { "$ref": "#/components/schemas/AssetVisibility" } - ] + ], + "description": "Asset visibility" } }, "required": [ @@ -15622,27 +16501,34 @@ "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" } }, @@ -15658,6 +16544,7 @@ "AssetMediaResponseDto": { "properties": { "id": { + "description": "Asset media ID", "type": "string" }, "status": { @@ -15665,7 +16552,8 @@ { "$ref": "#/components/schemas/AssetMediaStatus" } - ] + ], + "description": "Upload status" } }, "required": [ @@ -15676,6 +16564,7 @@ }, "AssetMediaSize": { "enum": [ + "original", "fullsize", "preview", "thumbnail" @@ -15683,6 +16572,7 @@ "type": "string" }, "AssetMediaStatus": { + "description": "Upload status", "enum": [ "created", "replaced", @@ -15693,6 +16583,7 @@ "AssetMetadataBulkDeleteDto": { "properties": { "items": { + "description": "Metadata items to delete", "items": { "$ref": "#/components/schemas/AssetMetadataBulkDeleteItemDto" }, @@ -15707,10 +16598,12 @@ "AssetMetadataBulkDeleteItemDto": { "properties": { "assetId": { + "description": "Asset ID", "format": "uuid", "type": "string" }, "key": { + "description": "Metadata key", "type": "string" } }, @@ -15723,16 +16616,20 @@ "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" } }, @@ -15747,6 +16644,7 @@ "AssetMetadataBulkUpsertDto": { "properties": { "items": { + "description": "Metadata items to upsert", "items": { "$ref": "#/components/schemas/AssetMetadataBulkUpsertItemDto" }, @@ -15761,13 +16659,16 @@ "AssetMetadataBulkUpsertItemDto": { "properties": { "assetId": { + "description": "Asset ID", "format": "uuid", "type": "string" }, "key": { + "description": "Metadata key", "type": "string" }, "value": { + "description": "Metadata value (object)", "type": "object" } }, @@ -15781,13 +16682,16 @@ "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" } }, @@ -15801,6 +16705,7 @@ "AssetMetadataUpsertDto": { "properties": { "items": { + "description": "Metadata items to upsert", "items": { "$ref": "#/components/schemas/AssetMetadataUpsertItemDto" }, @@ -15815,9 +16720,11 @@ "AssetMetadataUpsertItemDto": { "properties": { "key": { + "description": "Metadata key", "type": "string" }, "value": { + "description": "Metadata value (object)", "type": "object" } }, @@ -15910,6 +16817,7 @@ "type": "object" }, "AssetOrder": { + "description": "Asset sort order", "enum": [ "asc", "desc" @@ -15919,7 +16827,7 @@ "AssetResponseDto": { "properties": { "checksum": { - "description": "base64 encoded sha1 hash", + "description": "Base64 encoded SHA1 hash", "type": "string" }, "createdAt": { @@ -15929,16 +16837,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": { @@ -15957,25 +16869,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": [ @@ -15991,6 +16931,7 @@ "x-immich-state": "Deprecated" }, "livePhotoVideoId": { + "description": "Live photo video ID", "nullable": true, "type": "string" }, @@ -16001,18 +16942,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": { @@ -16023,6 +16968,7 @@ }, "resized": { "deprecated": true, + "description": "Is resized", "type": "boolean", "x-immich-history": [ { @@ -16051,6 +16997,7 @@ "type": "array" }, "thumbhash": { + "description": "Thumbhash for thumbnail generation", "nullable": true, "type": "string" }, @@ -16059,7 +17006,8 @@ { "$ref": "#/components/schemas/AssetTypeEnum" } - ] + ], + "description": "Asset type" }, "unassignedFaces": { "items": { @@ -16078,7 +17026,13 @@ { "$ref": "#/components/schemas/AssetVisibility" } - ] + ], + "description": "Asset visibility" + }, + "width": { + "description": "Asset width", + "nullable": true, + "type": "number" } }, "required": [ @@ -16090,8 +17044,10 @@ "fileCreatedAt", "fileModifiedAt", "hasMetadata", + "height", "id", "isArchived", + "isEdited", "isFavorite", "isOffline", "isTrashed", @@ -16102,19 +17058,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" } }, @@ -16128,12 +17088,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" } }, @@ -16145,6 +17108,7 @@ "type": "object" }, "AssetTypeEnum": { + "description": "Asset type", "enum": [ "IMAGE", "VIDEO", @@ -16154,6 +17118,7 @@ "type": "string" }, "AssetVisibility": { + "description": "Asset visibility", "enum": [ "archive", "timeline", @@ -16163,6 +17128,7 @@ "type": "string" }, "AudioCodec": { + "description": "Target audio codec", "enum": [ "mp3", "aac", @@ -16174,18 +17140,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" } }, @@ -16203,12 +17174,14 @@ { "$ref": "#/components/schemas/UserAvatarColor" } - ] + ], + "description": "Avatar color" } }, "type": "object" }, "BulkIdErrorReason": { + "description": "Error reason", "enum": [ "duplicate", "no_permission", @@ -16220,6 +17193,7 @@ "BulkIdResponseDto": { "properties": { "error": { + "description": "Error reason if failed", "enum": [ "duplicate", "no_permission", @@ -16229,9 +17203,11 @@ "type": "string" }, "id": { + "description": "ID", "type": "string" }, "success": { + "description": "Whether operation succeeded", "type": "boolean" } }, @@ -16244,6 +17220,7 @@ "BulkIdsDto": { "properties": { "ids": { + "description": "IDs to process", "items": { "format": "uuid", "type": "string" @@ -16259,9 +17236,11 @@ "CLIPConfig": { "properties": { "enabled": { + "description": "Whether the task is enabled", "type": "boolean" }, "modelName": { + "description": "Name of the model to use", "type": "string" } }, @@ -16272,6 +17251,7 @@ "type": "object" }, "CQMode": { + "description": "CQ mode", "enum": [ "auto", "cqp", @@ -16283,6 +17263,7 @@ "properties": { "gCastEnabled": { "default": false, + "description": "Whether Google Cast is enabled", "type": "boolean" } }, @@ -16294,6 +17275,7 @@ "CastUpdate": { "properties": { "gCastEnabled": { + "description": "Whether Google Cast is enabled", "type": "boolean" } }, @@ -16303,14 +17285,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" } @@ -16324,6 +17309,7 @@ "CheckExistingAssetsDto": { "properties": { "deviceAssetIds": { + "description": "Device asset IDs to check", "items": { "type": "string" }, @@ -16331,6 +17317,7 @@ "type": "array" }, "deviceId": { + "description": "Device ID", "type": "string" } }, @@ -16343,6 +17330,7 @@ "CheckExistingAssetsResponseDto": { "properties": { "existingIds": { + "description": "Existing asset IDs", "items": { "type": "string" }, @@ -16355,6 +17343,7 @@ "type": "object" }, "Colorspace": { + "description": "Colorspace", "enum": [ "srgb", "p3" @@ -16364,9 +17353,11 @@ "ContributorCountResponseDto": { "properties": { "assetCount": { + "description": "Number of assets contributed", "type": "integer" }, "userId": { + "description": "User ID", "type": "string" } }, @@ -16379,15 +17370,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" @@ -16395,6 +17389,7 @@ "type": "array" }, "description": { + "description": "Album description", "type": "string" } }, @@ -16406,6 +17401,7 @@ "CreateLibraryDto": { "properties": { "exclusionPatterns": { + "description": "Exclusion patterns (max 128)", "items": { "type": "string" }, @@ -16414,6 +17410,7 @@ "uniqueItems": true }, "importPaths": { + "description": "Import paths (max 128)", "items": { "type": "string" }, @@ -16422,9 +17419,11 @@ "uniqueItems": true }, "name": { + "description": "Library name", "type": "string" }, "ownerId": { + "description": "Owner user ID", "format": "uuid", "type": "string" } @@ -16437,6 +17436,7 @@ "CreateProfileImageDto": { "properties": { "file": { + "description": "Profile image file", "format": "binary", "type": "string" } @@ -16449,13 +17449,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" } }, @@ -16466,15 +17469,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" } @@ -16486,15 +17523,69 @@ ], "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" + }, "DownloadArchiveInfo": { "properties": { "assetIds": { + "description": "Asset IDs in this archive", "items": { "type": "string" }, "type": "array" }, "size": { + "description": "Archive size in bytes", "type": "integer" } }, @@ -16507,14 +17598,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" @@ -16522,6 +17616,7 @@ "type": "array" }, "userId": { + "description": "User ID to download assets from", "format": "uuid", "type": "string" } @@ -16531,10 +17626,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" } }, @@ -16547,12 +17644,14 @@ "DownloadResponseDto": { "properties": { "archives": { + "description": "Archive information", "items": { "$ref": "#/components/schemas/DownloadArchiveInfo" }, "type": "array" }, "totalSize": { + "description": "Total size in bytes", "type": "integer" } }, @@ -16565,10 +17664,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" } }, @@ -16577,9 +17678,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, @@ -16595,12 +17698,14 @@ "DuplicateResponseDto": { "properties": { "assets": { + "description": "Duplicate assets", "items": { "$ref": "#/components/schemas/AssetResponseDto" }, "type": "array" }, "duplicateId": { + "description": "Duplicate group ID", "type": "string" } }, @@ -16613,12 +17718,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" } }, @@ -16632,12 +17740,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" } }, @@ -16647,114 +17758,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" } @@ -16764,6 +17897,7 @@ "FaceDto": { "properties": { "id": { + "description": "Face ID", "format": "uuid", "type": "string" } @@ -16776,25 +17910,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" } }, @@ -16811,10 +17950,12 @@ "properties": { "enabled": { "default": false, + "description": "Whether folders are enabled", "type": "boolean" }, "sidebarWeb": { "default": false, + "description": "Whether folders appear in web sidebar", "type": "boolean" } }, @@ -16827,15 +17968,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" @@ -16849,7 +17993,8 @@ { "$ref": "#/components/schemas/ManualJobName" } - ] + ], + "description": "Job name" } }, "required": [ @@ -16858,6 +18003,7 @@ "type": "object" }, "JobName": { + "description": "Job name", "enum": [ "AssetDelete", "AssetDeleteCheck", @@ -16865,6 +18011,7 @@ "AssetDetectFaces", "AssetDetectDuplicatesQueueAll", "AssetDetectDuplicates", + "AssetEditThumbnailGeneration", "AssetEncodeVideoQueueAll", "AssetEncodeVideo", "AssetEmptyTrash", @@ -16920,6 +18067,7 @@ "JobSettingsDto": { "properties": { "concurrency": { + "description": "Concurrency", "minimum": 1, "type": "integer" } @@ -16932,39 +18080,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" } @@ -16986,19 +18143,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" } }, @@ -17013,9 +18174,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" } @@ -17029,13 +18192,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" } @@ -17061,11 +18227,13 @@ "LoginCredentialDto": { "properties": { "email": { + "description": "User email", "example": "testuser@email.com", "format": "email", "type": "string" }, "password": { + "description": "User password", "example": "password", "type": "string" } @@ -17079,27 +18247,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" } }, @@ -17118,9 +18294,11 @@ "LogoutResponseDto": { "properties": { "redirectUri": { + "description": "Redirect URI", "type": "string" }, "successful": { + "description": "Logout successful", "type": "boolean" } }, @@ -17133,6 +18311,7 @@ "MachineLearningAvailabilityChecksDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" }, "interval": { @@ -17150,15 +18329,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" } }, @@ -17167,15 +18350,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", @@ -17189,25 +18448,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" } @@ -17225,14 +18490,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" } @@ -17248,10 +18516,12 @@ "properties": { "duration": { "default": 5, + "description": "Memory duration in seconds", "type": "integer" }, "enabled": { "default": true, + "description": "Whether memories are enabled", "type": "boolean" } }, @@ -17264,10 +18534,12 @@ "MemoriesUpdate": { "properties": { "duration": { + "description": "Memory duration in seconds", "minimum": 1, "type": "integer" }, "enabled": { + "description": "Whether memories are enabled", "type": "boolean" } }, @@ -17276,6 +18548,7 @@ "MemoryCreateDto": { "properties": { "assetIds": { + "description": "Asset IDs to associate with memory", "items": { "format": "uuid", "type": "string" @@ -17286,13 +18559,16 @@ "$ref": "#/components/schemas/OnThisDayDto" }, "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" }, @@ -17301,7 +18577,8 @@ { "$ref": "#/components/schemas/MemoryType" } - ] + ], + "description": "Memory type" } }, "required": [ @@ -17320,6 +18597,7 @@ "type": "array" }, "createdAt": { + "description": "Creation date", "format": "date-time", "type": "string" }, @@ -17327,31 +18605,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" }, @@ -17360,9 +18646,11 @@ { "$ref": "#/components/schemas/MemoryType" } - ] + ], + "description": "Memory type" }, "updatedAt": { + "description": "Last update date", "format": "date-time", "type": "string" } @@ -17391,6 +18679,7 @@ "MemoryStatisticsResponseDto": { "properties": { "total": { + "description": "Total number of memories", "type": "integer" } }, @@ -17408,13 +18697,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" } @@ -17424,6 +18716,7 @@ "MergePersonDto": { "properties": { "ids": { + "description": "Person IDs to merge", "items": { "format": "uuid", "type": "string" @@ -17439,6 +18732,7 @@ "MetadataSearchDto": { "properties": { "albumIds": { + "description": "Filter by album IDs", "items": { "format": "uuid", "type": "string" @@ -17446,72 +18740,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": { @@ -17520,19 +18834,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" @@ -17540,23 +18859,28 @@ "type": "array" }, "previewPath": { + "description": "Filter by preview file path", "type": "string" }, "rating": { + "description": "Filter by rating", "maximum": 5, "minimum": -1, "type": "number" }, "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" @@ -17565,21 +18889,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" }, @@ -17588,13 +18917,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" }, @@ -17603,29 +18935,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" }, @@ -17634,14 +18997,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": { @@ -17649,9 +19015,11 @@ { "$ref": "#/components/schemas/NotificationType" } - ] + ], + "description": "Notification type" }, "userId": { + "description": "User ID to send notification to", "format": "uuid", "type": "string" } @@ -17665,6 +19033,7 @@ "NotificationDeleteAllDto": { "properties": { "ids": { + "description": "Notification IDs to delete", "items": { "format": "uuid", "type": "string" @@ -17680,16 +19049,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": { @@ -17697,13 +19070,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": { @@ -17711,7 +19087,8 @@ { "$ref": "#/components/schemas/NotificationType" } - ] + ], + "description": "Notification type" } }, "required": [ @@ -17746,6 +19123,7 @@ "NotificationUpdateAllDto": { "properties": { "ids": { + "description": "Notification IDs to update", "items": { "format": "uuid", "type": "string" @@ -17753,6 +19131,7 @@ "type": "array" }, "readAt": { + "description": "Date when notifications were read", "format": "date-time", "nullable": true, "type": "string" @@ -17766,6 +19145,7 @@ "NotificationUpdateDto": { "properties": { "readAt": { + "description": "Date when notification was read", "format": "date-time", "nullable": true, "type": "string" @@ -17776,6 +19156,7 @@ "OAuthAuthorizeResponseDto": { "properties": { "url": { + "description": "OAuth authorization URL", "type": "string" } }, @@ -17787,12 +19168,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" } }, @@ -17804,12 +19188,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" } }, @@ -17819,6 +19206,7 @@ "type": "object" }, "OAuthTokenEndpointAuthMethod": { + "description": "Token endpoint auth method", "enum": [ "client_secret_post", "client_secret_basic" @@ -17828,25 +19216,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" } }, @@ -17862,6 +19255,7 @@ "OnThisDayDto": { "properties": { "year": { + "description": "Year for on this day memory", "minimum": 1, "type": "number" } @@ -17874,6 +19268,7 @@ "OnboardingDto": { "properties": { "isOnboarded": { + "description": "Is user onboarded", "type": "boolean" } }, @@ -17885,6 +19280,7 @@ "OnboardingResponseDto": { "properties": { "isOnboarded": { + "description": "Is user onboarded", "type": "boolean" } }, @@ -17896,6 +19292,7 @@ "PartnerCreateDto": { "properties": { "sharedWithId": { + "description": "User ID to share with", "format": "uuid", "type": "string" } @@ -17919,25 +19316,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" } }, @@ -17954,6 +19358,7 @@ "PartnerUpdateDto": { "properties": { "inTimeline": { + "description": "Show partner assets in timeline", "type": "boolean" } }, @@ -17966,10 +19371,12 @@ "properties": { "enabled": { "default": true, + "description": "Whether people are enabled", "type": "boolean" }, "sidebarWeb": { "default": false, + "description": "Whether people appear in web sidebar", "type": "boolean" } }, @@ -17982,6 +19389,7 @@ "PeopleResponseDto": { "properties": { "hasNextPage": { + "description": "Whether there are more pages", "type": "boolean", "x-immich-history": [ { @@ -17996,15 +19404,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" } }, @@ -18018,9 +19429,11 @@ "PeopleUpdate": { "properties": { "enabled": { + "description": "Whether people are enabled", "type": "boolean" }, "sidebarWeb": { + "description": "Whether people appear in web sidebar", "type": "boolean" } }, @@ -18029,6 +19442,7 @@ "PeopleUpdateDto": { "properties": { "people": { + "description": "People to update", "items": { "$ref": "#/components/schemas/PeopleUpdateItem" }, @@ -18043,33 +19457,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" } }, @@ -18079,6 +19495,7 @@ "type": "object" }, "Permission": { + "description": "List of permissions", "enum": [ "all", "activity.create", @@ -18100,6 +19517,10 @@ "asset.upload", "asset.replace", "asset.copy", + "asset.derive", + "asset.edit.get", + "asset.edit.create", + "asset.edit.delete", "album.create", "album.read", "album.update", @@ -18115,12 +19536,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", @@ -18131,6 +19557,8 @@ "timeline.read", "timeline.download", "maintenance", + "map.read", + "map.search", "memory.create", "memory.read", "memory.update", @@ -18231,24 +19659,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" } }, @@ -18257,11 +19687,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": [ { @@ -18276,9 +19708,11 @@ "x-immich-state": "Stable" }, "id": { + "description": "Person ID", "type": "string" }, "isFavorite": { + "description": "Is favorite", "type": "boolean", "x-immich-history": [ { @@ -18293,15 +19727,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": [ @@ -18329,6 +19767,7 @@ "PersonStatisticsResponseDto": { "properties": { "assets": { + "description": "Number of assets", "type": "integer" } }, @@ -18340,29 +19779,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" } }, @@ -18371,11 +19812,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": [ { @@ -18390,15 +19833,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": [ { @@ -18413,15 +19859,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": [ @@ -18450,13 +19900,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" } @@ -18469,9 +19922,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" } @@ -18481,6 +19936,7 @@ "PinCodeSetupDto": { "properties": { "pinCode": { + "description": "PIN code (4-6 digits)", "example": "123456", "type": "string" } @@ -18493,18 +19949,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" } }, @@ -18518,28 +19979,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/PluginContextType" }, "type": "array" }, "title": { + "description": "Action title", "type": "string" } }, @@ -18555,6 +20023,7 @@ "type": "object" }, "PluginContextType": { + "description": "Context type", "enum": [ "asset", "album", @@ -18565,28 +20034,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/PluginContextType" }, "type": "array" }, "title": { + "description": "Filter title", "type": "string" } }, @@ -18604,39 +20080,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" } }, @@ -18661,14 +20147,16 @@ { "$ref": "#/components/schemas/PluginContextType" } - ] + ], + "description": "Context type" }, "type": { "allOf": [ { "$ref": "#/components/schemas/PluginTriggerType" } - ] + ], + "description": "Trigger type" } }, "required": [ @@ -18678,6 +20166,7 @@ "type": "object" }, "PluginTriggerType": { + "description": "Trigger type", "enum": [ "AssetCreate", "PersonRecognized" @@ -18687,9 +20176,11 @@ "PurchaseResponse": { "properties": { "hideBuyButtonUntil": { + "description": "Date until which to hide buy button", "type": "string" }, "showSupportBadge": { + "description": "Whether to show support badge", "type": "boolean" } }, @@ -18702,15 +20193,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", @@ -18727,9 +20221,11 @@ { "$ref": "#/components/schemas/QueueCommand" } - ] + ], + "description": "Queue command to execute" }, "force": { + "description": "Force the command execution (if applicable)", "type": "boolean" } }, @@ -18761,9 +20257,11 @@ "QueueJobResponseDto": { "properties": { "data": { + "description": "Job data payload", "type": "object" }, "id": { + "description": "Job ID", "type": "string" }, "name": { @@ -18771,9 +20269,11 @@ { "$ref": "#/components/schemas/JobName" } - ] + ], + "description": "Job name" }, "timestamp": { + "description": "Job creation timestamp", "type": "integer" } }, @@ -18813,13 +20313,15 @@ "notifications", "backupDatabase", "ocr", - "workflow" + "workflow", + "editor" ], "type": "string" }, "QueueResponseDto": { "properties": { "isPaused": { + "description": "Whether the queue is paused", "type": "boolean" }, "name": { @@ -18827,7 +20329,8 @@ { "$ref": "#/components/schemas/QueueName" } - ] + ], + "description": "Queue name" }, "statistics": { "$ref": "#/components/schemas/QueueStatisticsDto" @@ -18858,21 +20361,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" } }, @@ -18889,9 +20398,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" } }, @@ -18904,6 +20415,7 @@ "QueueUpdateDto": { "properties": { "isPaused": { + "description": "Whether to pause the queue", "type": "boolean" } }, @@ -18920,6 +20432,9 @@ "duplicateDetection": { "$ref": "#/components/schemas/QueueResponseLegacyDto" }, + "editor": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, "faceDetection": { "$ref": "#/components/schemas/QueueResponseLegacyDto" }, @@ -18967,6 +20482,7 @@ "backgroundTask", "backupDatabase", "duplicateDetection", + "editor", "faceDetection", "facialRecognition", "library", @@ -18987,6 +20503,7 @@ "RandomSearchDto": { "properties": { "albumIds": { + "description": "Filter by album IDs", "items": { "format": "uuid", "type": "string" @@ -18994,59 +20511,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" @@ -19054,20 +20587,24 @@ "type": "array" }, "rating": { + "description": "Filter by rating", "maximum": 5, "minimum": -1, "type": "number" }, "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" @@ -19076,18 +20613,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" }, @@ -19096,13 +20637,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" }, @@ -19111,18 +20655,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" } }, @@ -19132,6 +20681,7 @@ "properties": { "enabled": { "default": false, + "description": "Whether ratings are enabled", "type": "boolean" } }, @@ -19143,6 +20693,7 @@ "RatingsUpdate": { "properties": { "enabled": { + "description": "Whether ratings are enabled", "type": "boolean" } }, @@ -19165,10 +20716,12 @@ "ReverseGeocodingStateResponseDto": { "properties": { "lastImportFileName": { + "description": "Last import file name", "nullable": true, "type": "string" }, "lastUpdate": { + "description": "Last update timestamp", "nullable": true, "type": "string" } @@ -19179,9 +20732,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": { @@ -19197,6 +20763,7 @@ "type": "array" }, "total": { + "description": "Total number of matching albums", "type": "integer" } }, @@ -19211,6 +20778,7 @@ "SearchAssetResponseDto": { "properties": { "count": { + "description": "Number of assets in this page", "type": "integer" }, "facets": { @@ -19226,10 +20794,12 @@ "type": "array" }, "nextPage": { + "description": "Next page token", "nullable": true, "type": "string" }, "total": { + "description": "Total number of matching assets", "type": "integer" } }, @@ -19248,6 +20818,7 @@ "$ref": "#/components/schemas/AssetResponseDto" }, "value": { + "description": "Explore value", "type": "string" } }, @@ -19260,6 +20831,7 @@ "SearchExploreResponseDto": { "properties": { "fieldName": { + "description": "Explore field name", "type": "string" }, "items": { @@ -19278,9 +20850,11 @@ "SearchFacetCountResponseDto": { "properties": { "count": { + "description": "Number of assets with this facet value", "type": "integer" }, "value": { + "description": "Facet value", "type": "string" } }, @@ -19293,12 +20867,14 @@ "SearchFacetResponseDto": { "properties": { "counts": { + "description": "Facet counts", "items": { "$ref": "#/components/schemas/SearchFacetCountResponseDto" }, "type": "array" }, "fieldName": { + "description": "Facet field name", "type": "string" } }, @@ -19326,6 +20902,7 @@ "SearchStatisticsResponseDto": { "properties": { "total": { + "description": "Total number of matching assets", "type": "integer" } }, @@ -19348,66 +20925,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" } }, @@ -19421,15 +21019,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" } }, @@ -19444,36 +21046,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" } }, @@ -19495,48 +21108,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" } }, @@ -19562,18 +21190,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" }, @@ -19604,10 +21235,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" }, @@ -19630,16 +21263,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" } }, @@ -19656,27 +21292,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" } @@ -19695,6 +21338,7 @@ "ServerThemeDto": { "properties": { "customCss": { + "description": "Custom CSS for theming", "type": "string" } }, @@ -19706,13 +21350,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" } }, @@ -19726,12 +21373,15 @@ "ServerVersionResponseDto": { "properties": { "major": { + "description": "Major version number", "type": "integer" }, "minor": { + "description": "Minor version number", "type": "integer" }, "patch": { + "description": "Patch version number", "type": "integer" } }, @@ -19745,13 +21395,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" } @@ -19761,34 +21413,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" } }, @@ -19808,31 +21470,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" } }, @@ -19851,9 +21522,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" } @@ -19863,6 +21536,7 @@ "SessionUpdateDto": { "properties": { "isPendingSyncReset": { + "description": "Reset pending sync state", "type": "boolean" } }, @@ -19875,7 +21549,12 @@ { "$ref": "#/components/schemas/MaintenanceAction" } - ] + ], + "description": "Maintenance action" + }, + "restoreBackupFilename": { + "description": "Restore backup filename", + "type": "string" } }, "required": [ @@ -19886,17 +21565,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" @@ -19904,24 +21587,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" }, @@ -19930,7 +21618,8 @@ { "$ref": "#/components/schemas/SharedLinkType" } - ] + ], + "description": "Shared link type" } }, "required": [ @@ -19941,32 +21630,39 @@ "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" } @@ -19979,9 +21675,11 @@ "$ref": "#/components/schemas/AlbumResponseDto" }, "allowDownload": { + "description": "Allow downloads", "type": "boolean" }, "allowUpload": { + "description": "Allow uploads", "type": "boolean" }, "assets": { @@ -19991,36 +21689,45 @@ "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": { + "description": "Access token", "nullable": true, "type": "string" }, @@ -20029,9 +21736,11 @@ { "$ref": "#/components/schemas/SharedLinkType" } - ] + ], + "description": "Shared link type" }, "userId": { + "description": "Owner user ID", "type": "string" } }, @@ -20053,6 +21762,7 @@ "type": "object" }, "SharedLinkType": { + "description": "Shared link type", "enum": [ "ALBUM", "INDIVIDUAL" @@ -20063,10 +21773,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" } }, @@ -20079,9 +21791,11 @@ "SharedLinksUpdate": { "properties": { "enabled": { + "description": "Whether shared links are enabled", "type": "boolean" }, "sidebarWeb": { + "description": "Whether shared links appear in web sidebar", "type": "boolean" } }, @@ -20090,15 +21804,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" } @@ -20113,6 +21830,7 @@ "SmartSearchDto": { "properties": { "albumIds": { + "description": "Filter by album IDs", "items": { "format": "uuid", "type": "string" @@ -20120,66 +21838,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" @@ -20187,27 +21923,33 @@ "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", "maximum": 5, "minimum": -1, "type": "number" }, "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" @@ -20216,18 +21958,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" }, @@ -20236,13 +21982,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" }, @@ -20251,18 +22000,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", @@ -20273,7 +22026,7 @@ "StackCreateDto": { "properties": { "assetIds": { - "description": "first asset becomes the primary", + "description": "Asset IDs (first becomes primary, min 2)", "items": { "format": "uuid", "type": "string" @@ -20290,15 +22043,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" } }, @@ -20312,6 +22068,7 @@ "StackUpdateDto": { "properties": { "primaryAssetId": { + "description": "Primary asset ID", "format": "uuid", "type": "string" } @@ -20321,6 +22078,7 @@ "StatisticsSearchDto": { "properties": { "albumIds": { + "description": "Filter by album IDs", "items": { "format": "uuid", "type": "string" @@ -20328,62 +22086,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" @@ -20391,15 +22166,18 @@ "type": "array" }, "rating": { + "description": "Filter by rating", "maximum": 5, "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" @@ -20408,18 +22186,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" }, @@ -20428,13 +22210,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" }, @@ -20443,14 +22228,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" }, @@ -20462,6 +22261,7 @@ "SyncAckDto": { "properties": { "ack": { + "description": "Acknowledgment ID", "type": "string" }, "type": { @@ -20469,7 +22269,8 @@ { "$ref": "#/components/schemas/SyncEntityType" } - ] + ], + "description": "Sync entity type" } }, "required": [ @@ -20481,6 +22282,7 @@ "SyncAckSetDto": { "properties": { "acks": { + "description": "Acknowledgment IDs (max 1000)", "items": { "type": "string" }, @@ -20500,6 +22302,7 @@ "SyncAlbumDeleteV1": { "properties": { "albumId": { + "description": "Album ID", "type": "string" } }, @@ -20511,9 +22314,11 @@ "SyncAlbumToAssetDeleteV1": { "properties": { "albumId": { + "description": "Album ID", "type": "string" }, "assetId": { + "description": "Asset ID", "type": "string" } }, @@ -20526,9 +22331,11 @@ "SyncAlbumToAssetV1": { "properties": { "albumId": { + "description": "Album ID", "type": "string" }, "assetId": { + "description": "Asset ID", "type": "string" } }, @@ -20541,9 +22348,11 @@ "SyncAlbumUserDeleteV1": { "properties": { "albumId": { + "description": "Album ID", "type": "string" }, "userId": { + "description": "User ID", "type": "string" } }, @@ -20556,6 +22365,7 @@ "SyncAlbumUserV1": { "properties": { "albumId": { + "description": "Album ID", "type": "string" }, "role": { @@ -20563,9 +22373,11 @@ { "$ref": "#/components/schemas/AlbumUserRole" } - ] + ], + "description": "Album user role" }, "userId": { + "description": "User ID", "type": "string" } }, @@ -20579,19 +22391,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": { @@ -20602,13 +22419,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" } @@ -20629,6 +22449,7 @@ "SyncAssetDeleteV1": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" } }, @@ -20640,108 +22461,133 @@ "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" } @@ -20778,6 +22624,7 @@ "SyncAssetFaceDeleteV1": { "properties": { "assetFaceId": { + "description": "Asset face ID", "type": "string" } }, @@ -20789,6 +22636,7 @@ "SyncAssetFaceV1": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" }, "boundingBoxX1": { @@ -20804,6 +22652,7 @@ "type": "integer" }, "id": { + "description": "Asset face ID", "type": "string" }, "imageHeight": { @@ -20813,10 +22662,12 @@ "type": "integer" }, "personId": { + "description": "Person ID", "nullable": true, "type": "string" }, "sourceType": { + "description": "Source type", "type": "string" } }, @@ -20837,9 +22688,11 @@ "SyncAssetMetadataDeleteV1": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" }, "key": { + "description": "Key", "type": "string" } }, @@ -20852,12 +22705,15 @@ "SyncAssetMetadataV1": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" }, "key": { + "description": "Key", "type": "string" }, "value": { + "description": "Value", "type": "object" } }, @@ -20871,57 +22727,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" }, @@ -20930,14 +22809,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": [ @@ -20946,7 +22832,9 @@ "duration", "fileCreatedAt", "fileModifiedAt", + "height", "id", + "isEdited", "isFavorite", "libraryId", "livePhotoVideoId", @@ -20956,7 +22844,8 @@ "stackId", "thumbhash", "type", - "visibility" + "visibility", + "width" ], "type": "object" }, @@ -20968,36 +22857,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" }, @@ -21009,6 +22908,7 @@ "type": "integer" }, "storageLabel": { + "description": "User storage label", "nullable": true, "type": "string" } @@ -21035,6 +22935,7 @@ "type": "object" }, "SyncEntityType": { + "description": "Sync entity type", "enum": [ "AuthUserV1", "UserV1", @@ -21089,9 +22990,11 @@ "SyncMemoryAssetDeleteV1": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" }, "memoryId": { + "description": "Memory ID", "type": "string" } }, @@ -21104,9 +23007,11 @@ "SyncMemoryAssetV1": { "properties": { "assetId": { + "description": "Asset ID", "type": "string" }, "memoryId": { + "description": "Memory ID", "type": "string" } }, @@ -21119,6 +23024,7 @@ "SyncMemoryDeleteV1": { "properties": { "memoryId": { + "description": "Memory ID", "type": "string" } }, @@ -21130,41 +23036,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" @@ -21174,9 +23090,11 @@ { "$ref": "#/components/schemas/MemoryType" } - ] + ], + "description": "Memory type" }, "updatedAt": { + "description": "Updated at", "format": "date-time", "type": "string" } @@ -21200,9 +23118,11 @@ "SyncPartnerDeleteV1": { "properties": { "sharedById": { + "description": "Shared by ID", "type": "string" }, "sharedWithId": { + "description": "Shared with ID", "type": "string" } }, @@ -21215,12 +23135,15 @@ "SyncPartnerV1": { "properties": { "inTimeline": { + "description": "In timeline", "type": "boolean" }, "sharedById": { + "description": "Shared by ID", "type": "string" }, "sharedWithId": { + "description": "Shared with ID", "type": "string" } }, @@ -21234,6 +23157,7 @@ "SyncPersonDeleteV1": { "properties": { "personId": { + "description": "Person ID", "type": "string" } }, @@ -21245,38 +23169,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" } @@ -21296,6 +23230,7 @@ "type": "object" }, "SyncRequestType": { + "description": "Sync request types", "enum": [ "AlbumsV1", "AlbumUsersV1", @@ -21327,6 +23262,7 @@ "SyncStackDeleteV1": { "properties": { "stackId": { + "description": "Stack ID", "type": "string" } }, @@ -21338,19 +23274,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" } @@ -21367,9 +23308,11 @@ "SyncStreamDto": { "properties": { "reset": { + "description": "Reset sync state", "type": "boolean" }, "types": { + "description": "Sync request types", "items": { "$ref": "#/components/schemas/SyncRequestType" }, @@ -21384,6 +23327,7 @@ "SyncUserDeleteV1": { "properties": { "userId": { + "description": "User ID", "type": "string" } }, @@ -21399,9 +23343,11 @@ { "$ref": "#/components/schemas/UserMetadataKey" } - ] + ], + "description": "User metadata key" }, "userId": { + "description": "User ID", "type": "string" } }, @@ -21418,12 +23364,15 @@ { "$ref": "#/components/schemas/UserMetadataKey" } - ] + ], + "description": "User metadata key" }, "userId": { + "description": "User ID", "type": "string" }, "value": { + "description": "User metadata value", "type": "object" } }, @@ -21442,26 +23391,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" } @@ -21586,30 +23542,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" @@ -21619,27 +23581,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" @@ -21649,9 +23618,11 @@ { "$ref": "#/components/schemas/AudioCodec" } - ] + ], + "description": "Target audio codec" }, "targetResolution": { + "description": "Target resolution", "type": "string" }, "targetVideoCodec": { @@ -21659,12 +23630,15 @@ { "$ref": "#/components/schemas/VideoCodec" } - ] + ], + "description": "Target video codec" }, "temporalAQ": { + "description": "Temporal AQ", "type": "boolean" }, "threads": { + "description": "Threads", "minimum": 0, "type": "integer" }, @@ -21673,16 +23647,19 @@ { "$ref": "#/components/schemas/ToneMapping" } - ] + ], + "description": "Tone mapping" }, "transcode": { "allOf": [ { "$ref": "#/components/schemas/TranscodePolicy" } - ] + ], + "description": "Transcode policy" }, "twoPass": { + "description": "Two pass", "type": "boolean" } }, @@ -21714,6 +23691,7 @@ "SystemConfigFacesDto": { "properties": { "import": { + "description": "Import", "type": "boolean" } }, @@ -21725,6 +23703,7 @@ "SystemConfigGeneratedFullsizeImageDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" }, "format": { @@ -21732,9 +23711,16 @@ { "$ref": "#/components/schemas/ImageFormat" } - ] + ], + "description": "Image format" + }, + "progressive": { + "default": false, + "description": "Progressive", + "type": "boolean" }, "quality": { + "description": "Quality", "maximum": 100, "minimum": 1, "type": "integer" @@ -21754,14 +23740,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" } @@ -21780,9 +23773,11 @@ { "$ref": "#/components/schemas/Colorspace" } - ] + ], + "description": "Colorspace" }, "extractEmbedded": { + "description": "Extract embedded", "type": "boolean" }, "fullsize": { @@ -21809,6 +23804,9 @@ "backgroundTask": { "$ref": "#/components/schemas/JobSettingsDto" }, + "editor": { + "$ref": "#/components/schemas/JobSettingsDto" + }, "faceDetection": { "$ref": "#/components/schemas/JobSettingsDto" }, @@ -21848,6 +23846,7 @@ }, "required": [ "backgroundTask", + "editor", "faceDetection", "library", "metadataExtraction", @@ -21884,6 +23883,7 @@ "type": "string" }, "enabled": { + "description": "Enabled", "type": "boolean" } }, @@ -21896,6 +23896,7 @@ "SystemConfigLibraryWatchDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" } }, @@ -21907,6 +23908,7 @@ "SystemConfigLoggingDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" }, "level": { @@ -21935,6 +23937,7 @@ "$ref": "#/components/schemas/DuplicateDetectionConfig" }, "enabled": { + "description": "Enabled", "type": "boolean" }, "facialRecognition": { @@ -21971,6 +23974,7 @@ "type": "string" }, "enabled": { + "description": "Enabled", "type": "boolean" }, "lightStyle": { @@ -21999,6 +24003,7 @@ "SystemConfigNewVersionCheckDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" } }, @@ -22010,21 +24015,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" } }, @@ -22052,58 +24062,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" }, @@ -22112,7 +24138,8 @@ { "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod" } - ] + ], + "description": "Token endpoint auth method" } }, "required": [ @@ -22140,6 +24167,7 @@ "SystemConfigPasswordLoginDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" } }, @@ -22151,6 +24179,7 @@ "SystemConfigReverseGeocodingDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" } }, @@ -22162,13 +24191,16 @@ "SystemConfigServerDto": { "properties": { "externalDomain": { + "description": "External domain", "format": "uri", "type": "string" }, "loginPageMessage": { + "description": "Login page message", "type": "string" }, "publicUsers": { + "description": "Public users", "type": "boolean" } }, @@ -22182,12 +24214,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": { @@ -22205,23 +24240,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" } }, @@ -22238,12 +24279,15 @@ "SystemConfigStorageTemplateDto": { "properties": { "enabled": { + "description": "Enabled", "type": "boolean" }, "hashVerificationEnabled": { + "description": "Hash verification enabled", "type": "boolean" }, "template": { + "description": "Template", "type": "string" } }, @@ -22276,48 +24320,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" }, @@ -22350,6 +24402,7 @@ "SystemConfigThemeDto": { "properties": { "customCss": { + "description": "Custom CSS for theming", "type": "string" } }, @@ -22361,10 +24414,12 @@ "SystemConfigTrashDto": { "properties": { "days": { + "description": "Days", "minimum": 0, "type": "integer" }, "enabled": { + "description": "Enabled", "type": "boolean" } }, @@ -22377,6 +24432,7 @@ "SystemConfigUserDto": { "properties": { "deleteDelay": { + "description": "Delete delay", "minimum": 1, "type": "integer" } @@ -22389,6 +24445,7 @@ "TagBulkAssetsDto": { "properties": { "assetIds": { + "description": "Asset IDs", "items": { "format": "uuid", "type": "string" @@ -22396,6 +24453,7 @@ "type": "array" }, "tagIds": { + "description": "Tag IDs", "items": { "format": "uuid", "type": "string" @@ -22412,6 +24470,7 @@ "TagBulkAssetsResponseDto": { "properties": { "count": { + "description": "Number of assets tagged", "type": "integer" } }, @@ -22423,13 +24482,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" @@ -22443,26 +24505,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" } }, @@ -22478,6 +24547,7 @@ "TagUpdateDto": { "properties": { "color": { + "description": "Tag color (hex)", "nullable": true, "type": "string" } @@ -22487,6 +24557,7 @@ "TagUpsertDto": { "properties": { "tags": { + "description": "Tag names to upsert", "items": { "type": "string" }, @@ -22502,10 +24573,12 @@ "properties": { "enabled": { "default": true, + "description": "Whether tags are enabled", "type": "boolean" }, "sidebarWeb": { "default": true, + "description": "Whether tags appear in web sidebar", "type": "boolean" } }, @@ -22518,9 +24591,11 @@ "TagsUpdate": { "properties": { "enabled": { + "description": "Whether tags are enabled", "type": "boolean" }, "sidebarWeb": { + "description": "Whether tags appear in web sidebar", "type": "boolean" } }, @@ -22529,6 +24604,7 @@ "TemplateDto": { "properties": { "template": { + "description": "Template name", "type": "string" } }, @@ -22540,9 +24616,11 @@ "TemplateResponseDto": { "properties": { "html": { + "description": "Template HTML content", "type": "string" }, "name": { + "description": "Template name", "type": "string" } }, @@ -22555,6 +24633,7 @@ "TestEmailResponseDto": { "properties": { "messageId": { + "description": "Email message ID", "type": "string" } }, @@ -22590,7 +24669,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" }, @@ -22745,6 +24824,7 @@ "type": "object" }, "ToneMapping": { + "description": "Tone mapping", "enum": [ "hable", "mobius", @@ -22754,6 +24834,7 @@ "type": "string" }, "TranscodeHWAccel": { + "description": "Transcode hardware acceleration", "enum": [ "nvenc", "qsv", @@ -22764,6 +24845,7 @@ "type": "string" }, "TranscodePolicy": { + "description": "Transcode policy", "enum": [ "all", "optimal", @@ -22776,6 +24858,7 @@ "TrashResponseDto": { "properties": { "count": { + "description": "Number of items in trash", "type": "integer" } }, @@ -22787,16 +24870,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": { @@ -22804,7 +24891,8 @@ { "$ref": "#/components/schemas/AssetOrder" } - ] + ], + "description": "Asset sort order" } }, "type": "object" @@ -22816,7 +24904,8 @@ { "$ref": "#/components/schemas/AlbumUserRole" } - ] + ], + "description": "Album user role" } }, "required": [ @@ -22827,26 +24916,33 @@ "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", "maximum": 5, "minimum": -1, "type": "number" @@ -22856,7 +24952,8 @@ { "$ref": "#/components/schemas/AssetVisibility" } - ] + ], + "description": "Asset visibility" } }, "type": "object" @@ -22864,6 +24961,7 @@ "UpdateLibraryDto": { "properties": { "exclusionPatterns": { + "description": "Exclusion patterns (max 128)", "items": { "type": "string" }, @@ -22872,6 +24970,7 @@ "uniqueItems": true }, "importPaths": { + "description": "Import paths (max 128)", "items": { "type": "string" }, @@ -22880,6 +24979,7 @@ "uniqueItems": true }, "name": { + "description": "Library name", "type": "string" } }, @@ -22888,32 +24988,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" } }, @@ -22937,34 +25045,43 @@ "$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" }, "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" } @@ -22979,6 +25096,7 @@ "UserAdminDeleteDto": { "properties": { "force": { + "description": "Force delete even if user has assets", "type": "boolean" } }, @@ -22991,24 +25109,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": { @@ -23017,32 +25141,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": { @@ -23050,13 +25182,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" } @@ -23090,36 +25225,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" } @@ -23127,6 +25271,7 @@ "type": "object" }, "UserAvatarColor": { + "description": "Avatar color", "enum": [ "primary", "pink", @@ -23144,13 +25289,16 @@ "UserLicense": { "properties": { "activatedAt": { + "description": "Activation date", "format": "date-time", "type": "string" }, "activationKey": { + "description": "Activation key", "type": "string" }, "licenseKey": { + "description": "License key", "type": "string" } }, @@ -23162,6 +25310,7 @@ "type": "object" }, "UserMetadataKey": { + "description": "User metadata key", "enum": [ "preferences", "license", @@ -23268,22 +25417,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" } }, @@ -23298,6 +25453,7 @@ "type": "object" }, "UserStatus": { + "description": "User status", "enum": [ "active", "removing", @@ -23313,16 +25469,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" } }, @@ -23331,6 +25491,7 @@ "ValidateAccessTokenResponseDto": { "properties": { "authStatus": { + "description": "Authentication status", "type": "boolean" } }, @@ -23342,6 +25503,7 @@ "ValidateLibraryDto": { "properties": { "exclusionPatterns": { + "description": "Exclusion patterns (max 128)", "items": { "type": "string" }, @@ -23350,6 +25512,7 @@ "uniqueItems": true }, "importPaths": { + "description": "Import paths to validate (max 128)", "items": { "type": "string" }, @@ -23363,13 +25526,16 @@ "ValidateLibraryImportPathResponseDto": { "properties": { "importPath": { + "description": "Import path", "type": "string" }, "isValid": { "default": false, + "description": "Is valid", "type": "boolean" }, "message": { + "description": "Validation message", "type": "string" } }, @@ -23382,6 +25548,7 @@ "ValidateLibraryResponseDto": { "properties": { "importPaths": { + "description": "Validation results for import paths", "items": { "$ref": "#/components/schemas/ValidateLibraryImportPathResponseDto" }, @@ -23393,10 +25560,12 @@ "VersionCheckStateResponseDto": { "properties": { "checkedAt": { + "description": "Last check timestamp", "nullable": true, "type": "string" }, "releaseVersion": { + "description": "Release version", "nullable": true, "type": "string" } @@ -23408,6 +25577,7 @@ "type": "object" }, "VideoCodec": { + "description": "Target video codec", "enum": [ "h264", "hevc", @@ -23417,6 +25587,7 @@ "type": "string" }, "VideoContainer": { + "description": "Accepted containers", "enum": [ "mov", "mp4", @@ -23428,9 +25599,11 @@ "WorkflowActionItemDto": { "properties": { "actionConfig": { + "description": "Action configuration", "type": "object" }, "pluginActionId": { + "description": "Plugin action ID", "format": "uuid", "type": "string" } @@ -23443,19 +25616,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" } }, @@ -23471,24 +25649,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": { @@ -23496,7 +25679,8 @@ { "$ref": "#/components/schemas/PluginTriggerType" } - ] + ], + "description": "Workflow trigger type" } }, "required": [ @@ -23510,9 +25694,11 @@ "WorkflowFilterItemDto": { "properties": { "filterConfig": { + "description": "Filter configuration", "type": "object" }, "pluginFilterId": { + "description": "Plugin filter ID", "format": "uuid", "type": "string" } @@ -23525,19 +25711,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" } }, @@ -23553,34 +25744,42 @@ "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": { @@ -23588,7 +25787,8 @@ { "$ref": "#/components/schemas/PluginTriggerType" } - ] + ], + "description": "Workflow trigger type" } }, "required": [ @@ -23607,24 +25807,29 @@ "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": { @@ -23632,7 +25837,8 @@ { "$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 248216ad5b..3fe3b1570a 100644 --- a/open-api/typescript-sdk/.nvmrc +++ b/open-api/typescript-sdk/.nvmrc @@ -1 +1 @@ -24.12.0 +24.13.0 diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 832093fe23..2d30f4fbd8 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.1", + "version": "2.5.2", "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.4", + "@types/node": "^24.10.9", "typescript": "^5.3.3" }, "repository": { @@ -28,6 +28,6 @@ "directory": "open-api/typescript-sdk" }, "volta": { - "node": "24.12.0" + "node": "24.13.0" } } diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index afc583f91d..d8c960a393 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.1 + * 2.5.2 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -15,172 +15,315 @@ 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; + /** 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 +340,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,330 +420,586 @@ 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 */ 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 = { + /** 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; + /** 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 asset ID */ duplicateId?: string | null; + /** Asset IDs to update */ ids: string[]; + /** Mark as favorite */ isFavorite?: boolean; + /** Latitude coordinate */ latitude?: number; + /** Longitude coordinate */ longitude?: number; + /** Rating */ rating?: number; + /** 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 */ rating?: number; + /** 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 AssetEditActionCrop = { + /** Type of edit action to perform */ + action: AssetEditAction; + parameters: CropParameters; +}; +export type RotateParameters = { + /** Rotation angle in degrees */ + angle: number; +}; +export type AssetEditActionRotate = { + /** Type of edit action to perform */ + action: AssetEditAction; + parameters: RotateParameters; +}; +export type MirrorParameters = { + /** Axis to mirror along */ + axis: MirrorAxis; +}; +export type AssetEditActionMirror = { + /** Type of edit action to perform */ + action: AssetEditAction; + parameters: MirrorParameters; +}; +export type AssetEditsDto = { + /** Asset ID to apply edits to */ + assetId: string; + /** List of edit actions to apply (crop, rotate, or mirror) */ + edits: (AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror)[]; +}; +export type AssetEditActionListDto = { + /** List of edit actions to apply (crop, rotate, or mirror) */ + edits: (AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror)[]; +}; export type AssetMetadataResponseDto = { + /** 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 = { @@ -609,136 +1029,221 @@ 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 = { + /** Asset IDs */ assetIds: string[]; }; 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 = { @@ -749,6 +1254,7 @@ export type QueuesResponseLegacyDto = { backgroundTask: QueueResponseLegacyDto; backupDatabase: QueueResponseLegacyDto; duplicateDetection: QueueResponseLegacyDto; + editor: QueueResponseLegacyDto; faceDetection: QueueResponseLegacyDto; facialRecognition: QueueResponseLegacyDto; library: QueueResponseLegacyDto; @@ -765,238 +1271,359 @@ 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; + /** Is memory saved */ isSaved?: boolean; + /** Memory date */ memoryAt: string; + /** Date when memory was seen */ seenAt?: 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; + /** 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; + /** 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 = { @@ -1004,84 +1631,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; + /** Filter by rating */ rating?: number; + /** 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 = { @@ -1089,189 +1775,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[]; + /** Filter by rating */ rating?: number; + /** 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; + /** Filter by rating */ rating?: number; + /** 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[]; + /** Filter by rating */ rating?: 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; }; 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 = {}; @@ -1279,211 +2127,340 @@ 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 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 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; @@ -1499,9 +2476,11 @@ export type SystemConfigJobDto = { }; export type SystemConfigLibraryScanDto = { cronExpression: string; + /** Enabled */ enabled: boolean; }; export type SystemConfigLibraryWatchDto = { + /** Enabled */ enabled: boolean; }; export type SystemConfigLibraryDto = { @@ -1509,40 +2488,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; @@ -1550,63 +2546,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 = { @@ -1618,13 +2647,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 = { @@ -1651,38 +2684,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 = { @@ -1692,7 +2744,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[]; @@ -1730,77 +2782,463 @@ 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; + /** 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 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 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 */ @@ -1876,6 +3314,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 */ @@ -1888,6 +3383,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 */ @@ -1903,6 +3409,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 */ @@ -2581,6 +4098,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: AssetEditsDto; + }>(`/assets/${encodeURIComponent(id)}/edits`, { + ...opts + })); +} +/** + * Apply edits to an existing asset + */ +export function editAsset({ id, assetEditActionListDto }: { + id: string; + assetEditActionListDto: AssetEditActionListDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetEditsDto; + }>(`/assets/${encodeURIComponent(id)}/edits`, oazapfts.json({ + ...opts, + method: "PUT", + body: assetEditActionListDto + }))); +} /** * Get asset metadata */ @@ -2652,7 +4209,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; @@ -2661,6 +4219,7 @@ export function downloadAsset({ id, key, slug }: { status: 200; data: Blob; }>(`/assets/${encodeURIComponent(id)}/original${QS.query(QS.explode({ + edited, key, slug }))}`, { @@ -2691,7 +4250,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; @@ -2701,6 +4261,7 @@ export function viewAsset({ id, key, size, slug }: { status: 200; data: Blob; }>(`/assets/${encodeURIComponent(id)}/thumbnail${QS.query(QS.explode({ + edited, key, size, slug @@ -5209,7 +6770,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", @@ -5288,6 +6859,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", @@ -5303,12 +6878,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", @@ -5319,6 +6899,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", @@ -5433,7 +7015,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" @@ -5463,7 +7055,8 @@ export enum QueueName { Notifications = "notifications", BackupDatabase = "backupDatabase", Ocr = "ocr", - Workflow = "workflow" + Workflow = "workflow", + Editor = "editor" } export enum QueueCommand { Start = "start", @@ -5508,6 +7101,7 @@ export enum JobName { AssetDetectFaces = "AssetDetectFaces", AssetDetectDuplicatesQueueAll = "AssetDetectDuplicatesQueueAll", AssetDetectDuplicates = "AssetDetectDuplicates", + AssetEditThumbnailGeneration = "AssetEditThumbnailGeneration", AssetEncodeVideoQueueAll = "AssetEncodeVideoQueueAll", AssetEncodeVideo = "AssetEncodeVideo", AssetEmptyTrash = "AssetEmptyTrash", @@ -5709,3 +7303,8 @@ export enum OAuthTokenEndpointAuthMethod { ClientSecretPost = "client_secret_post", ClientSecretBasic = "client_secret_basic" } +export enum UserMetadataKey { + Preferences = "preferences", + License = "license", + Onboarding = "onboarding" +} diff --git a/package.json b/package.json index 9ebfc6dd04..dadfba8bf0 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "immich-monorepo", - "version": "0.0.1", + "version": "2.5.2", "description": "Monorepo for Immich", "private": true, - "packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a", + "packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48", "engines": { "pnpm": ">=10.0.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9980023d36..9b9f7fcaf5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,7 @@ importers: devDependencies: prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.0 cli: dependencies: @@ -36,7 +36,7 @@ importers: version: 1.20.1 lodash-es: specifier: ^4.17.21 - version: 4.17.22 + version: 4.17.23 micromatch: specifier: ^4.0.8 version: 4.0.8 @@ -63,11 +63,11 @@ importers: specifier: ^4.13.1 version: 4.13.4 '@types/node': - specifier: ^24.10.4 - version: 24.10.4 + specifier: ^24.10.9 + version: 24.10.9 '@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@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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 @@ -85,7 +85,7 @@ importers: version: 10.1.8(eslint@9.39.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@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) @@ -97,28 +97,28 @@ importers: version: 5.5.0 prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.0 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.0)(typescript@5.9.3) typescript: specifier: ^5.3.3 version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.53.0(eslint@9.39.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)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite-tsconfig-paths: specifier: ^6.0.0 - version: 6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(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@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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.8)(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.8)(react@18.3.1))(@types/react@19.2.8)(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.8)(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.8)(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.8)(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.8)(react@18.3.1) autoprefixer: specifier: ^10.4.17 version: 10.4.23(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.8)(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 @@ -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.0 typescript: specifier: ^5.1.6 version: 5.9.3 @@ -197,7 +200,7 @@ importers: version: 9.39.2 '@faker-js/faker': specifier: ^10.1.0 - version: 10.1.0 + version: 10.2.0 '@immich/cli': specifier: file:../cli version: link:../cli @@ -217,8 +220,8 @@ importers: specifier: ^3.4.2 version: 3.7.1 '@types/node': - specifier: ^24.10.4 - version: 24.10.4 + specifier: ^24.10.9 + version: 24.10.9 '@types/pg': specifier: ^8.15.1 version: 8.16.0 @@ -239,7 +242,7 @@ importers: version: 10.1.8(eslint@9.39.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@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) @@ -254,16 +257,16 @@ importers: version: 3.7.2 pg: specifier: ^8.11.3 - version: 8.16.3 + version: 8.17.1 pngjs: specifier: ^7.0.0 version: 7.0.0 prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.0 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.0)(typescript@5.9.3) sharp: specifier: ^0.34.5 version: 0.34.5 @@ -272,19 +275,19 @@ importers: 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.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.53.0(eslint@9.39.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@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) e2e-auth-server: devDependencies: @@ -305,10 +308,10 @@ importers: devDependencies: prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.0 prettier-plugin-sort-json: specifier: ^4.1.1 - version: 4.1.1(prettier@3.7.4) + version: 4.2.0(prettier@3.8.0) open-api/typescript-sdk: dependencies: @@ -317,8 +320,8 @@ importers: version: 1.1.0 devDependencies: '@types/node': - specifier: ^24.10.4 - version: 24.10.4 + specifier: ^24.10.9 + version: 24.10.9 typescript: specifier: ^5.3.3 version: 5.9.3 @@ -342,58 +345,58 @@ importers: version: 2.0.0-rc13 '@nestjs/bullmq': specifier: ^11.0.1 - version: 11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(bullmq@5.66.4) + version: 11.0.4(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(bullmq@5.66.5) '@nestjs/common': specifier: ^11.0.4 - version: 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.12(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.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^11.0.4 - version: 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) '@nestjs/platform-socket.io': specifier: ^11.0.4 - version: 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.11)(rxjs@7.8.2) + version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.12)(rxjs@7.8.2) '@nestjs/schedule': specifier: ^6.0.0 - version: 6.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + version: 6.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) '@nestjs/swagger': specifier: ^11.0.2 - version: 11.2.3(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + version: 11.2.5(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) '@nestjs/websockets': specifier: ^11.0.4 - version: 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-socket.io@11.1.12)(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.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-prometheus': - specifier: ^0.208.0 - version: 0.208.0(@opentelemetry/api@1.9.0) + specifier: ^0.210.0 + version: 0.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-http': - specifier: ^0.208.0 - version: 0.208.0(@opentelemetry/api@1.9.0) + specifier: ^0.210.0 + version: 0.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-ioredis': - specifier: ^0.57.0 - version: 0.57.0(@opentelemetry/api@1.9.0) + specifier: ^0.58.0 + version: 0.58.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.56.0 + version: 0.56.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-pg': - specifier: ^0.61.0 - version: 0.61.2(@opentelemetry/api@1.9.0) + specifier: ^0.62.0 + version: 0.62.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0) + version: 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': specifier: ^2.0.1 - version: 2.2.0(@opentelemetry/api@1.9.0) + version: 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': - specifier: ^0.208.0 - version: 0.208.0(@opentelemetry/api@1.9.0) + specifier: ^0.210.0 + version: 0.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': specifier: ^1.34.0 version: 1.38.0 @@ -420,10 +423,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.4 + version: 5.66.5 chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -459,7 +462,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 @@ -468,7 +471,7 @@ importers: version: 7.14.0 ioredis: specifier: ^5.8.2 - version: 5.8.2 + version: 5.9.1 jose: specifier: ^5.10.0 version: 5.10.0 @@ -483,10 +486,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 @@ -498,16 +501,16 @@ importers: version: 2.0.2 nest-commander: specifier: ^3.16.0 - version: 3.20.1(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@types/inquirer@8.2.12)(@types/node@24.10.4)(typescript@5.9.3) + version: 3.20.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@types/inquirer@8.2.12)(@types/node@24.10.9)(typescript@5.9.3) nestjs-cls: specifier: ^5.0.0 - version: 5.4.3(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 5.4.3(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-kysely: specifier: 3.1.2 - version: 3.1.2(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(kysely@0.28.2)(reflect-metadata@0.2.2) + version: 3.1.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(kysely@0.28.2)(reflect-metadata@0.2.2) nestjs-otel: specifier: ^7.0.0 - version: 7.0.1(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + version: 7.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) nodemailer: specifier: ^7.0.0 version: 7.0.12 @@ -516,16 +519,16 @@ importers: version: 6.8.1 pg: specifier: ^8.11.3 - version: 8.16.3 + version: 8.17.1 pg-connection-string: specifier: ^2.9.1 - version: 2.9.1 + version: 2.10.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 @@ -565,9 +568,12 @@ importers: 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.8 uuid: specifier: ^11.1.0 version: 11.1.0 @@ -580,13 +586,13 @@ importers: version: 9.39.2 '@nestjs/cli': specifier: ^11.0.2 - version: 11.0.14(@swc/core@1.15.8(@swc/helpers@0.5.17))(@types/node@24.10.4) + version: 11.0.15(@swc/core@1.15.8(@swc/helpers@0.5.17))(@types/node@24.10.9) '@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.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-express@11.1.11) + version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-express@11.1.12) '@swc/core': specifier: ^1.4.14 version: 1.15.8(@swc/helpers@0.5.17) @@ -622,7 +628,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 @@ -633,11 +639,11 @@ importers: specifier: ^2.0.0 version: 2.0.0 '@types/node': - specifier: ^24.10.4 - version: 24.10.4 + specifier: ^24.10.9 + version: 24.10.9 '@types/nodemailer': specifier: ^7.0.0 - version: 7.0.4 + version: 7.0.5 '@types/picomatch': specifier: ^4.0.0 version: 4.0.2 @@ -646,7 +652,7 @@ importers: version: 6.0.5 '@types/react': specifier: ^19.0.0 - version: 19.2.7 + version: 19.2.8 '@types/sanitize-html': specifier: ^2.13.0 version: 2.16.0 @@ -664,7 +670,7 @@ 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@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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) @@ -673,7 +679,7 @@ importers: version: 10.1.8(eslint@9.39.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@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) @@ -691,16 +697,16 @@ importers: version: 7.0.0 prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.0 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.0)(typescript@5.9.3) sql-formatter: specifier: ^15.0.0 - version: 15.6.12 + version: 15.7.0 supertest: specifier: ^7.1.0 - version: 7.1.4 + version: 7.2.2 tailwindcss: specifier: ^3.4.0 version: 3.4.19(tsx@4.21.0)(yaml@2.8.2) @@ -712,22 +718,22 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) unplugin-swc: specifier: ^1.4.5 - version: 1.5.9(@swc/core@1.15.8(@swc/helpers@0.5.17))(rollup@4.53.4) + version: 1.5.9(@swc/core@1.15.8(@swc/helpers@0.5.17))(rollup@4.55.1) vite-tsconfig-paths: specifier: ^6.0.0 - version: 6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(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@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) web: dependencies: '@formatjs/icu-messageformat-parser': specifier: ^3.0.0 - version: 3.2.1 + version: 3.3.0 '@immich/justified-layout-wasm': specifier: ^0.4.3 version: 0.4.3 @@ -735,8 +741,8 @@ importers: specifier: file:../open-api/typescript-sdk version: link:../open-api/typescript-sdk '@immich/ui': - specifier: ^0.54.0 - version: 0.54.0(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1) + specifier: ^0.59.0 + version: 0.59.0(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -745,22 +751,22 @@ importers: version: 7.4.47 '@photo-sphere-viewer/core': specifier: ^5.14.0 - version: 5.14.0 + version: 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 @@ -769,7 +775,7 @@ importers: version: 0.41.4 '@zoom-image/svelte': specifier: ^0.3.0 - version: 0.3.8(svelte@5.46.1) + version: 0.3.8(svelte@5.48.0) dom-to-image: specifier: ^2.6.0 version: 2.6.0 @@ -787,22 +793,22 @@ importers: version: 4.7.8 happy-dom: specifier: ^20.0.0 - version: 20.0.11 + version: 20.3.0 intl-messageformat: specifier: ^11.0.0 - version: 11.0.8 + version: 11.0.9 justified-layout: specifier: ^4.1.0 version: 4.1.0 lodash-es: specifier: ^4.17.21 - version: 4.17.22 + version: 4.17.23 luxon: specifier: ^3.4.4 version: 3.7.2 maplibre-gl: specifier: ^5.6.2 - version: 5.15.0 + version: 5.16.0 pmtiles: specifier: ^4.3.0 version: 4.3.2 @@ -820,22 +826,25 @@ importers: version: 5.2.2 svelte-i18n: specifier: ^4.0.1 - version: 4.0.1(svelte@5.46.1) + version: 4.0.1(svelte@5.48.0) svelte-jsoneditor: specifier: ^3.10.0 - version: 3.11.0(svelte@5.46.1) + version: 3.11.0(svelte@5.48.0) svelte-maplibre: specifier: ^1.2.5 - version: 1.2.5(svelte@5.46.1) + version: 1.2.5(svelte@5.48.0) svelte-persisted-store: specifier: ^0.12.0 - version: 0.12.0(svelte@5.46.1) + version: 0.12.0(svelte@5.48.0) 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 @@ -845,7 +854,7 @@ importers: version: 9.39.2 '@faker-js/faker': specifier: ^10.0.0 - version: 10.1.0 + version: 10.2.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) @@ -854,25 +863,25 @@ importers: 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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))) + version: 3.0.10(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.53.4)(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 0.9.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: 6.2.4 + version: 6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.1.18(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.3.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(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.0.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 5.3.1(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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) @@ -896,7 +905,7 @@ 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@25.0.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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 @@ -911,7 +920,7 @@ importers: version: 6.0.2(eslint@9.39.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.46.1) + version: 3.14.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.48.0) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.2(jiti@2.6.1)) @@ -923,28 +932,28 @@ importers: version: 16.5.0 prettier: specifier: ^3.7.4 - version: 3.7.4 + version: 3.8.0 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.0)(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.0) prettier-plugin-svelte: specifier: ^3.3.3 - version: 3.4.1(prettier@3.7.4)(svelte@5.46.1) + version: 3.4.1(prettier@3.8.0)(svelte@5.48.0) 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.46.1 - version: 5.46.1 + specifier: 5.48.0 + version: 5.48.0 svelte-check: specifier: ^4.1.5 - version: 4.3.5(picomatch@4.0.3)(svelte@5.46.1)(typescript@5.9.3) + version: 4.3.5(picomatch@4.0.3)(svelte@5.48.0)(typescript@5.9.3) svelte-eslint-parser: specifier: ^1.3.3 - version: 1.4.1(svelte@5.46.1) + version: 1.4.1(svelte@5.48.0) tailwindcss: specifier: ^4.1.7 version: 4.1.18 @@ -953,13 +962,13 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.45.0 - version: 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.1.2 - version: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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@25.0.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages: @@ -1104,6 +1113,9 @@ 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'} + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} @@ -1120,124 +1132,124 @@ packages: '@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-sesv2@3.971.0': + resolution: {integrity: sha512-NP/lbf3mfY10Txzl0ml2YnTjnZwflp1+faOotMCrXi4fb6kInosdW0ZSHXNlNulFo9cW+llq07lD59Sw3nny+A==} + engines: {node: '>=20.0.0'} - '@aws-sdk/client-sso@3.948.0': - resolution: {integrity: sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A==} - engines: {node: '>=18.0.0'} + '@aws-sdk/client-sso@3.971.0': + resolution: {integrity: sha512-Xx+w6DQqJxDdymYyIxyKJnRzPvVJ4e/Aw0czO7aC9L/iraaV7AG8QtRe93OGW6aoHSh72CIiinnpJJfLsQqP4g==} + engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.947.0': - resolution: {integrity: sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/core@3.970.0': + resolution: {integrity: sha512-klpzObldOq8HXzDjDlY6K8rMhYZU6mXRz6P9F9N+tWnjoYFfeBMra8wYApydElTUYQKP1O7RLHwH1OKFfKcqIA==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.947.0': - resolution: {integrity: sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-env@3.970.0': + resolution: {integrity: sha512-rtVzXzEtAfZBfh+lq3DAvRar4c3jyptweOAJR2DweyXx71QSMY+O879hjpMwES7jl07a3O1zlnFIDo4KP/96kQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.947.0': - resolution: {integrity: sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-http@3.970.0': + resolution: {integrity: sha512-CjDbWL7JxjLc9ZxQilMusWSw05yRvUJKRpz59IxDpWUnSMHC9JMMUUkOy5Izk8UAtzi6gupRWArp4NG4labt9Q==} + engines: {node: '>=20.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-ini@3.971.0': + resolution: {integrity: sha512-c0TGJG4xyfTZz3SInXfGU8i5iOFRrLmy4Bo7lMyH+IpngohYMYGYl61omXqf2zdwMbDv+YJ9AviQTcCaEUKi8w==} + engines: {node: '>=20.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-login@3.971.0': + resolution: {integrity: sha512-yhbzmDOsk0RXD3rTPhZra4AWVnVAC4nFWbTp+sUty1hrOPurUmhuz8bjpLqYTHGnlMbJp+UqkQONhS2+2LzW2g==} + engines: {node: '>=20.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-node@3.971.0': + resolution: {integrity: sha512-epUJBAKivtJqalnEBRsYIULKYV063o/5mXNJshZfyvkAgNIzc27CmmKRXTN4zaNOZg8g/UprFp25BGsi19x3nQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.947.0': - resolution: {integrity: sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-process@3.970.0': + resolution: {integrity: sha512-0XeT8OaT9iMA62DFV9+m6mZfJhrD0WNKf4IvsIpj2Z7XbaYfz3CoDDvNoALf3rPY9NzyMHgDxOspmqdvXP00mw==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.952.0': - resolution: {integrity: sha512-1CQdP5RzxeXuEfytbAD5TgreY1c9OacjtCdO8+n9m05tpzBABoNBof0hcjzw1dtrWFH7deyUgfwCl1TAN3yBWQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-sso@3.971.0': + resolution: {integrity: sha512-dY0hMQ7dLVPQNJ8GyqXADxa9w5wNfmukgQniLxGVn+dMRx3YLViMp5ZpTSQpFhCWNF0oKQrYAI5cHhUJU1hETw==} + engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.952.0': - resolution: {integrity: sha512-5hJbfaZdHDAP8JlwplNbXJAat9Vv7L0AbTZzkbPIgjHhC3vrMf5r3a6I1HWFp5i5pXo7J45xyuf5uQGZJxJlCg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-web-identity@3.971.0': + resolution: {integrity: sha512-F1AwfNLr7H52T640LNON/h34YDiMuIqW/ZreGzhRR6vnFGaSPtNSKAKB2ssAMkLM8EVg8MjEAYD3NCUiEo+t/w==} + engines: {node: '>=20.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-host-header@3.969.0': + resolution: {integrity: sha512-AWa4rVsAfBR4xqm7pybQ8sUNJYnjyP/bJjfAw34qPuh3M9XrfGbAHG0aiAfQGrBnmS28jlO6Kz69o+c6PRw1dw==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-logger@3.936.0': - resolution: {integrity: sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-logger@3.969.0': + resolution: {integrity: sha512-xwrxfip7Y2iTtCMJ+iifN1E1XMOuhxIHY9DreMCvgdl4r7+48x2S1bCYPWH3eNY85/7CapBWdJ8cerpEl12sQQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-recursion-detection@3.948.0': - resolution: {integrity: sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-recursion-detection@3.969.0': + resolution: {integrity: sha512-2r3PuNquU3CcS1Am4vn/KHFwLi8QFjMdA/R+CRDXT4AFO/0qxevF/YStW3gAKntQIgWgQV8ZdEtKAoJvLI4UWg==} + engines: {node: '>=20.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-sdk-s3@3.970.0': + resolution: {integrity: sha512-v/Y5F1lbFFY7vMeG5yYxuhnn0CAshz6KMxkz1pDyPxejNE9HtA0w8R6OTBh/bVdIm44QpjhbI7qeLdOE/PLzXQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-user-agent@3.947.0': - resolution: {integrity: sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-user-agent@3.970.0': + resolution: {integrity: sha512-dnSJGGUGSFGEX2NzvjwSefH+hmZQ347AwbLhAsi0cdnISSge+pcGfOFrJt2XfBIypwFe27chQhlfuf/gWdzpZg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.952.0': - resolution: {integrity: sha512-OtuirjxuOqZyDcI0q4WtoyWfkq3nSnbH41JwJQsXJefduWcww1FQe5TL1JfYCU7seUxHzK8rg2nFxUBuqUlZtg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/nested-clients@3.971.0': + resolution: {integrity: sha512-TWaILL8GyYlhGrxxnmbkazM4QsXatwQgoWUvo251FXmUOsiXDFDVX3hoGIfB3CaJhV2pJPfebHUNJtY6TjZ11g==} + engines: {node: '>=20.0.0'} - '@aws-sdk/region-config-resolver@3.936.0': - resolution: {integrity: sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/region-config-resolver@3.969.0': + resolution: {integrity: sha512-scj9OXqKpcjJ4jsFLtqYWz3IaNvNOQTFFvEY8XMJXTv+3qF5I7/x9SJtKzTRJEBF3spjzBUYPtGFbs9sj4fisQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/signature-v4-multi-region@3.947.0': - resolution: {integrity: sha512-UaYmzoxf9q3mabIA2hc4T6x5YSFUG2BpNjAZ207EA1bnQMiK+d6vZvb83t7dIWL/U1de1sGV19c1C81Jf14rrA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/signature-v4-multi-region@3.970.0': + resolution: {integrity: sha512-z3syXfuK/x/IsKf/AeYmgc2NT7fcJ+3fHaGO+fkghkV9WEba3fPyOwtTBX4KpFMNb2t50zDGZwbzW1/5ighcUQ==} + engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.952.0': - resolution: {integrity: sha512-IpQVC9WOeXQlCEcFVNXWDIKy92CH1Az37u9K0H3DF/HT56AjhyDVKQQfHUy00nt7bHFe3u0K5+zlwErBeKy5ZA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/token-providers@3.971.0': + resolution: {integrity: sha512-4hKGWZbmuDdONMJV0HJ+9jwTDb0zLfKxcCLx2GEnBY31Gt9GeyIQ+DZ97Bb++0voawj6pnZToFikXTyrEq2x+w==} + engines: {node: '>=20.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/types@3.969.0': + resolution: {integrity: sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ==} + engines: {node: '>=20.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-arn-parser@3.968.0': + resolution: {integrity: sha512-gqqvYcitIIM2K4lrDX9de9YvOfXBcVdxfT/iLnvHJd4YHvSXlt+gs+AsL4FfPCxG4IG9A+FyulP9Sb1MEA75vw==} + engines: {node: '>=20.0.0'} - '@aws-sdk/util-endpoints@3.936.0': - resolution: {integrity: sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==} - engines: {node: '>=18.0.0'} + '@aws-sdk/util-endpoints@3.970.0': + resolution: {integrity: sha512-TZNZqFcMUtjvhZoZRtpEGQAdULYiy6rcGiXAbLU7e9LSpIYlRqpLa207oMNfgbzlL2PnHko+eVg8rajDiSOYCg==} + engines: {node: '>=20.0.0'} - '@aws-sdk/util-locate-window@3.893.0': - resolution: {integrity: sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==} - engines: {node: '>=18.0.0'} + '@aws-sdk/util-locate-window@3.965.2': + resolution: {integrity: sha512-qKgO7wAYsXzhwCHhdbaKFyxd83Fgs8/1Ka+jjSPrv2Ll7mB55Wbwlo0kkfMLh993/yEc8aoDIAc1Fz9h4Spi4Q==} + engines: {node: '>=20.0.0'} - '@aws-sdk/util-user-agent-browser@3.936.0': - resolution: {integrity: sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==} + '@aws-sdk/util-user-agent-browser@3.969.0': + resolution: {integrity: sha512-bpJGjuKmFr0rA6UKUCmN8D19HQFMLXMx5hKBXqBlPFdalMhxJSjcxzX9DbQh0Fn6bJtxCguFmRGOBdQqNOt49g==} - '@aws-sdk/util-user-agent-node@3.947.0': - resolution: {integrity: sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==} - engines: {node: '>=18.0.0'} + '@aws-sdk/util-user-agent-node@3.971.0': + resolution: {integrity: sha512-Eygjo9mFzQYjbGY3MYO6CsIhnTwAMd3WmuFalCykqEmj2r5zf0leWrhPaqvA5P68V5JdGfPYgj7vhNOd6CtRBQ==} + engines: {node: '>=20.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==} + '@aws-sdk/xml-builder@3.969.0': + resolution: {integrity: sha512-BSe4Lx/qdRQQdX8cSSI7Et20vqBspzAjBy8ZmXVoyLkol3y4sXBXzn+BiLtR+oh60ExQn6o2DU4QjdOZbXaKIQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.3': + resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} 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.28.6': + resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} engines: {node: '>=6.9.0'} '@babel/compat-data@7.28.5': @@ -1790,8 +1802,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': @@ -1816,6 +1828,24 @@ packages: '@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==} @@ -2308,6 +2338,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'} @@ -2837,8 +2878,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.2.0': + resolution: {integrity: sha512-rTXwAsIxpCqzUnZvrxVh3L0QA0NzToqWBLAhV+zDV3MIIwiQhAZHMdPCIaj5n/yADu/tyk12wIPgL6YHGXJP+g==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} '@fig/complete-commander@3.2.0': @@ -2858,32 +2899,32 @@ packages: '@formatjs/ecma402-abstract@2.3.6': resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} - '@formatjs/ecma402-abstract@3.0.7': - resolution: {integrity: sha512-U55Yulf37vBXN0C7gHm7hrxULVrcrhpQBcdLmIN2rpYpLfC5eIpa1JRX9efjU74gfzjK/MSmSG3Lxv3E4ZNZIw==} + '@formatjs/ecma402-abstract@3.0.8': + resolution: {integrity: sha512-NRiqvxAvhbARZRFSRFPjN0y8txxmVutv2vMYvW2HSdCVf58w9l4osLj6Ujif643vImwZBcbKqhiKE0IOhY+DvA==} '@formatjs/fast-memoize@2.2.7': resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} - '@formatjs/fast-memoize@3.0.2': - resolution: {integrity: sha512-YFApUDWFmjpPwAE7VcY7PYVjm6JaLZOAo0UfCQj1/OGi/1QtduG9kIBHmVC551M6AI01qvuP5kjbDebrZOT4Vg==} + '@formatjs/fast-memoize@3.0.3': + resolution: {integrity: sha512-CArYtQKGLAOruCMeq5/RxCg6vUXFx3OuKBdTm30Wn/+gCefehmZ8Y2xSMxMrO2iel7hRyE3HKfV56t3vAU6D4Q==} '@formatjs/icu-messageformat-parser@2.11.4': resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} - '@formatjs/icu-messageformat-parser@3.2.1': - resolution: {integrity: sha512-DEECn8HEHtI4dvfKtTfvDOZ9nCTAJ2ESXGPRGKe4dkn/RE9w/G0NjgP/kFAQJbwIKWHo+BRxpee1bQKJ4lF6pg==} + '@formatjs/icu-messageformat-parser@3.3.0': + resolution: {integrity: sha512-dqxGSwH22ZfBwa6EVvrrIo+8kHHUSjuw9iZy6HkkN5XgH5/8ny9zDGhvC6ZOFYp01PAbwHvUTIHqznC6Z1nIbA==} '@formatjs/icu-skeleton-parser@1.8.16': resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} - '@formatjs/icu-skeleton-parser@2.0.7': - resolution: {integrity: sha512-/LEeQ2gOU7ujm7LJk07OYYOpsOtIH/6ma78vTHvZNGZ6m0wn3gxQqU39HEpXZfez6aIhGh7Psde2H2ILj5wb0Q==} + '@formatjs/icu-skeleton-parser@2.0.8': + resolution: {integrity: sha512-Z493tGxtKu0xNcSZjS8HrWNfq25HMscqbq5qwRFBYz14b70k1DHmhqVAwYDdDK0Ytj9YG1nvY4+IRq53LVNFdA==} '@formatjs/intl-localematcher@0.6.2': resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} - '@formatjs/intl-localematcher@0.7.4': - resolution: {integrity: sha512-AWsSZupIBMU/y04Nj24CjohyNVyfItMJPxSzX5OJwedDEIbGLOHkPxCjAeLeiLF2dw4xmQA8psktdi9MaebBQw==} + '@formatjs/intl-localematcher@0.7.5': + resolution: {integrity: sha512-7/nd90cn5CT7SVF71/ybUKAcnvBlr9nZlJJp8O8xIZHXFgYOC4SXExZlSdgHv2l6utjw1byidL06QzChvQMHwA==} '@fortawesome/fontawesome-common-types@7.1.0': resolution: {integrity: sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==} @@ -2939,6 +2980,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'} @@ -3084,8 +3131,8 @@ packages: peerDependencies: svelte: ^5.0.0 - '@immich/ui@0.54.0': - resolution: {integrity: sha512-6jvkvKhgsZ7LvspaJkbht/f8W5IRm+vjYkcZecShFAPaxaowbm7io9sO15MpJdIQfPdXg7vwLI527PV3vlBc6A==} + '@immich/ui@0.59.0': + resolution: {integrity: sha512-7yxvyhhd99T0AHhjMakp7c/U4n0jGAmRO5xpncsRASRvqZve/LAibjr6N5FJc5IAd222DROTMLn6imsxVfqfvg==} peerDependencies: svelte: ^5.0.0 @@ -3093,6 +3140,10 @@ packages: resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} + '@inquirer/ansi@2.0.3': + resolution: {integrity: sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/checkbox@4.3.2': resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} engines: {node: '>=18'} @@ -3102,6 +3153,15 @@ packages: '@types/node': optional: true + '@inquirer/checkbox@5.0.4': + resolution: {integrity: sha512-DrAMU3YBGMUAp6ArwTIp/25CNDtDbxk7UjIrrtM25JVVrlVYlVzHh5HR1BDFu9JMyUoZ4ZanzeaHqNDttf3gVg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/confirm@5.1.21': resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} engines: {node: '>=18'} @@ -3111,6 +3171,15 @@ packages: '@types/node': optional: true + '@inquirer/confirm@6.0.4': + resolution: {integrity: sha512-WdaPe7foUnoGYvXzH4jp4wH/3l+dBhZ3uwhKjXjwdrq5tEIFaANxj6zrGHxLdsIA0yKM0kFPVcEalOZXBB5ISA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/core@10.3.2': resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} engines: {node: '>=18'} @@ -3120,6 +3189,15 @@ packages: '@types/node': optional: true + '@inquirer/core@11.1.1': + resolution: {integrity: sha512-hV9o15UxX46OyQAtaoMqAOxGR8RVl1aZtDx1jHbCtSJy1tBdTfKxLPKf7utsE4cRy4tcmCQ4+vdV+ca+oNxqNA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/editor@4.2.23': resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} engines: {node: '>=18'} @@ -3129,6 +3207,15 @@ packages: '@types/node': optional: true + '@inquirer/editor@5.0.4': + resolution: {integrity: sha512-QI3Jfqcv6UO2/VJaEFONH8Im1ll++Xn/AJTBn9Xf+qx2M+H8KZAdQ5sAe2vtYlo+mLW+d7JaMJB4qWtK4BG3pw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/expand@4.0.23': resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} engines: {node: '>=18'} @@ -3138,6 +3225,15 @@ packages: '@types/node': optional: true + '@inquirer/expand@5.0.4': + resolution: {integrity: sha512-0I/16YwPPP0Co7a5MsomlZLpch48NzYfToyqYAOWtBmaXSB80RiNQ1J+0xx2eG+Wfxt0nHtpEWSRr6CzNVnOGg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -3147,10 +3243,23 @@ packages: '@types/node': optional: true + '@inquirer/external-editor@2.0.3': + resolution: {integrity: sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/figures@1.0.15': resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} engines: {node: '>=18'} + '@inquirer/figures@2.0.3': + resolution: {integrity: sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/input@4.3.1': resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} engines: {node: '>=18'} @@ -3160,6 +3269,15 @@ packages: '@types/node': optional: true + '@inquirer/input@5.0.4': + resolution: {integrity: sha512-4B3s3jvTREDFvXWit92Yc6jF1RJMDy2VpSqKtm4We2oVU65YOh2szY5/G14h4fHlyQdpUmazU5MPCFZPRJ0AOw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/number@3.0.23': resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} engines: {node: '>=18'} @@ -3169,6 +3287,15 @@ packages: '@types/node': optional: true + '@inquirer/number@4.0.4': + resolution: {integrity: sha512-CmMp9LF5HwE+G/xWsC333TlCzYYbXMkcADkKzcawh49fg2a1ryLc7JL1NJYYt1lJ+8f4slikNjJM9TEL/AljYQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/password@4.0.23': resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} engines: {node: '>=18'} @@ -3178,9 +3305,9 @@ packages: '@types/node': optional: true - '@inquirer/prompts@7.10.1': - resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} - engines: {node: '>=18'} + '@inquirer/password@5.0.4': + resolution: {integrity: sha512-ZCEPyVYvHK4W4p2Gy6sTp9nqsdHQCfiPXIP9LbJVW4yCinnxL/dDDmPaEZVysGrj8vxVReRnpfS2fOeODe9zjg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: @@ -3196,6 +3323,15 @@ packages: '@types/node': optional: true + '@inquirer/prompts@8.2.0': + resolution: {integrity: sha512-rqTzOprAj55a27jctS3vhvDDJzYXsr33WXTjODgVOru21NvBo9yIgLIAf7SBdSV0WERVly3dR6TWyp7ZHkvKFA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/rawlist@4.1.11': resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} engines: {node: '>=18'} @@ -3205,6 +3341,15 @@ packages: '@types/node': optional: true + '@inquirer/rawlist@5.2.0': + resolution: {integrity: sha512-CciqGoOUMrFo6HxvOtU5uL8fkjCmzyeB6fG7O1vdVAZVSopUBYECOwevDBlqNLyyYmzpm2Gsn/7nLrpruy9RFg==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/search@3.2.2': resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} engines: {node: '>=18'} @@ -3214,6 +3359,15 @@ packages: '@types/node': optional: true + '@inquirer/search@4.1.0': + resolution: {integrity: sha512-EAzemfiP4IFvIuWnrHpgZs9lAhWDA0GM3l9F4t4mTQ22IFtzfrk8xbkMLcAN7gmVML9O/i+Hzu8yOUyAaL6BKA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/select@4.4.2': resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} engines: {node: '>=18'} @@ -3223,6 +3377,15 @@ packages: '@types/node': optional: true + '@inquirer/select@5.0.4': + resolution: {integrity: sha512-s8KoGpPYMEQ6WXc0dT9blX2NtIulMdLOO3LA1UKOiv7KFWzlJ6eLkEYTDBIi+JkyKXyn8t/CD6TinxGjyLt57g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/type@3.0.10': resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} engines: {node: '>=18'} @@ -3232,11 +3395,20 @@ packages: '@types/node': optional: true + '@inquirer/type@4.0.3': + resolution: {integrity: sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@internationalized/date@3.10.0': resolution: {integrity: sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==} - '@ioredis/commands@1.4.0': - resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} + '@ioredis/commands@1.5.0': + resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} @@ -3459,6 +3631,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==} @@ -3508,8 +3683,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.15': + resolution: {integrity: sha512-4Sw4i+PRI1CGVnl3F15GWytFYD+QHs6vsayVeqDhhWwL1a7ZhQyUYvmlCMoWi77rZA0+m3ObUO1WujtkXsYBDQ==} engines: {node: '>= 20.11'} hasBin: true peerDependencies: @@ -3521,8 +3696,8 @@ packages: '@swc/core': optional: true - '@nestjs/common@11.1.11': - resolution: {integrity: sha512-R/+A8XFqLgN8zNs2twhrOaE7dJbRQhdPX3g46am4RT/x8xGLqDphrXkUIno4cGUZHxbczChBAaAPTdPv73wDZA==} + '@nestjs/common@11.1.12': + resolution: {integrity: sha512-v6U3O01YohHO+IE3EIFXuRuu3VJILWzyMmSYZXpyBbnp0hk0mFyHxK2w3dF4I5WnbwiRbWlEXdeXFvPQ7qaZzw==} peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -3534,8 +3709,8 @@ packages: class-validator: optional: true - '@nestjs/core@11.1.11': - resolution: {integrity: sha512-H9i+zT3RvHi7tDc+lCmWHJ3ustXveABCr+Vcpl96dNOxgmrx4elQSTC4W93Mlav2opfLV+p0UTHY6L+bpUA4zA==} + '@nestjs/core@11.1.12': + resolution: {integrity: sha512-97DzTYMf5RtGAVvX1cjwpKRiCUpkeQ9CCzSAenqkAhOmNVVFaApbhuw+xrDt13rsCa2hHVOYPrV4dBgOYMJjsA==} engines: {node: '>= 20'} peerDependencies: '@nestjs/common': ^11.0.0 @@ -3565,14 +3740,14 @@ packages: class-validator: optional: true - '@nestjs/platform-express@11.1.11': - resolution: {integrity: sha512-kyABSskdMRIAMWL0SlbwtDy4yn59RL4HDdwHDz/fxWuv7/53YP8Y2DtV3/sHqY5Er0msMVTZrM38MjqXhYL7gw==} + '@nestjs/platform-express@11.1.12': + resolution: {integrity: sha512-GYK/vHI0SGz5m8mxr7v3Urx8b9t78Cf/dj5aJMZlGd9/1D9OI1hAl00BaphjEXINUJ/BQLxIlF2zUjrYsd6enQ==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 - '@nestjs/platform-socket.io@11.1.11': - resolution: {integrity: sha512-0z6pLg9CuTXtz7q2lRZoPOU94DN28OTa39f4cQrlZysKA6QrKM7w7z6xqb4g32qjF+LQHFNRmMJtE/pLrxBaig==} + '@nestjs/platform-socket.io@11.1.12': + resolution: {integrity: sha512-1itTTYsAZecrq2NbJOkch32y8buLwN7UpcNRdJrhlS+ovJ5GxLx3RyJ3KylwBhbYnO5AeYyL1U/i4W5mg/4qDA==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/websockets': ^11.0.0 @@ -3589,10 +3764,10 @@ packages: peerDependencies: typescript: '>=4.8.2' - '@nestjs/swagger@11.2.3': - resolution: {integrity: sha512-a0xFfjeqk69uHIUpP8u0ryn4cKuHdra2Ug96L858i0N200Hxho+n3j+TlQXyOF4EstLSGjTfxI1Xb2E1lUxeNg==} + '@nestjs/swagger@11.2.5': + resolution: {integrity: sha512-wCykbEybMqiYcvkyzPW4SbXKcwra9AGdajm0MvFgKR3W+gd1hfeKlo67g/s9QCRc/mqUU4KOE5Qtk7asMeFuiA==} 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: '*' @@ -3606,8 +3781,8 @@ packages: class-validator: optional: true - '@nestjs/testing@11.1.11': - resolution: {integrity: sha512-Po2aZKXlxuySDEh3Gi05LJ7/BtfTAPRZ3KPTrbpNrTmgGr3rFgEGYpQwN50wXYw0pywoICiFLZSZ/qXsplf6NA==} + '@nestjs/testing@11.1.12': + resolution: {integrity: sha512-W0M/i5nb9qRQpTQfJm+1mGT/+y4YezwwdcD7mxFG8JEZ5fz/ZEAk1Ayri2VBJKJUdo20B1ggnvqew4dlTMrSNg==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3619,8 +3794,8 @@ packages: '@nestjs/platform-express': optional: true - '@nestjs/websockets@11.1.11': - resolution: {integrity: sha512-apuP7C/gtMBIYNgA8IWt75GTZeWya5JQCnrLZFcOu+IZt00j9Xd/Bm7hbj/Qr/JVoM/7q6c/4p4oOZtBGx4aeA==} + '@nestjs/websockets@11.1.12': + resolution: {integrity: sha512-ulSOYcgosx1TqY425cRC5oXtAu1R10+OSmVfgyR9ueR25k4luekURt8dzAZxhxSCI0OsDj9WKCFLTkEuAwg0wg==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3663,88 +3838,94 @@ packages: '@oazapfts/runtime@1.1.0': resolution: {integrity: sha512-PwCn69pexqg/uhc0bpEHSlRFdfTtSnq3icXHd0wf4BQwZSMKsCerTnydzegVScEegYkokzIxMcl9li7on86A2w==} - '@opentelemetry/api-logs@0.208.0': - resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} + '@opentelemetry/api-logs@0.210.0': + resolution: {integrity: sha512-CMtLxp+lYDriveZejpBND/2TmadrrhUfChyxzmkFtHaMDdSKfP59MAYyA0ICBvEBdm3iXwLcaj/8Ic/pnGw9Yg==} 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.210.0': + resolution: {integrity: sha512-tM0ROS/hZM72kB55cSjDcghVcUXBJdGkGzpkhD7M1B/gpcvZPSGfjFgKN3dgmxNgF76NxtbUwv3ik0wS+Kz52g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/context-async-hooks@2.4.0': + resolution: {integrity: sha512-jn0phJ+hU7ZuvaoZE/8/Euw3gvHJrn2yi+kXrymwObEPVPjtwCmkvXDRQCWli+fCTTF/aSOtXaLr7CLIvv3LQg==} 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.4.0': + resolution: {integrity: sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==} 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.210.0': + resolution: {integrity: sha512-+BolenqOO6ow65go7uWRYPvvs/BBIWp1mtRn93VvGduqvMVH/IY8nXrt80a4L9hZ7lHi2Tq2/NcC3H2QzcWKag==} 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.210.0': + resolution: {integrity: sha512-Q8/SEQtgrErbVVRg9M9iaG8m5wdPNdU0UOF7U43sAhwfmPG92ZOk/aenKhg0DXSNJHhkCDNCgS1kSoErAB3z0A==} 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.210.0': + resolution: {integrity: sha512-Y/yPc+gDhsWB7AsNzQWxblw4ULbvhCycMaQ2aAn+HSAVbgbMiZa0SbclPVHSnpnNzKSLVavFjweAr0pQA1KKLg==} 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.210.0': + resolution: {integrity: sha512-pWZ/Tjrqev9rdkqe8F6A9FGddLZrjl6iRAU5LBvvRL6I3PSgG8z1xM0cESAy1jzAF4wGohnAh8rB7hHzpUOYEA==} 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.210.0': + resolution: {integrity: sha512-JpLThG8Hh8A/Jzdzw9i4Ftu+EzvLaX/LouN+mOOHmadL0iror0Qsi3QWzucXeiUsDDsiYgjfKyi09e6sltytgA==} 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.210.0': + resolution: {integrity: sha512-CFa7SOinYOVWIWJuQL7XFeyedzmFGIpHpSMNFE8Xefb6iGB4m+MukQecdssvPcJKYlfF5FpovEOLXwafAzsXWQ==} 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.210.0': + resolution: {integrity: sha512-8i+7d70Hho6pcheTtbqIuS+bo+AIX/oNUTMwIEZoehUE4ZdbGmeVaE+hJS2LAErFeFaU71w164lAgYyMUEQ8zw==} 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.210.0': + resolution: {integrity: sha512-1GPLOyxIfUX24WM8Oea+vx9d9TlewposUnsQXTjusxVMQ/dWvt5JIDJyTsfNDS412XRUOORgF97PwsfDY5QKGA==} 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.210.0': + resolution: {integrity: sha512-9JkyaCl70anEtuKZdoCQmjDuz1/paEixY/DWfsvHt7PGKq3t8/nQ/6/xwxHjG+SkPAUbo1Iq4h7STe7Pk2bc5A==} 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.210.0': + resolution: {integrity: sha512-qVUY7Hsm/t5buGOtPcTV1Ch4W9kj2wGaQaAF5FO4XR8TMKl2GM45tUCnr0/1dF3wo4RG9khMxrddeQWdRL4fIg==} 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.4.0': + resolution: {integrity: sha512-qpiXY0TUEFjBBp9b1na9LfuVQw6W8LH+te7uv+CC+0Up78ZDtZZwOjK2M7CL7Nspnw+yS4JdgEA7oxsBu0Ctsg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -3755,62 +3936,62 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-http@0.208.0': - resolution: {integrity: sha512-rhmK46DRWEbQQB77RxmVXGyjs6783crXCnFjYQj+4tDH/Kpv9Rbg3h2kaNyp5Vz2emF1f9HOQQvZoHzwMWOFZQ==} + '@opentelemetry/instrumentation-http@0.210.0': + resolution: {integrity: sha512-dICO+0D0VBnrDOmDXOvpmaP0gvai6hNhJ5y6+HFutV0UoXc7pMgJlJY3O7AzT725cW/jP38ylmfHhQa7M0Nhww==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-ioredis@0.57.0': - resolution: {integrity: sha512-o/PYGPbfFbS0Sq8EEQC8YUgDMiTGvwoMejPjV2d466yJoii+BUpffGejVQN0hC5V5/GT29m1B1jL+3yruNxwDw==} + '@opentelemetry/instrumentation-ioredis@0.58.0': + resolution: {integrity: sha512-2tEJFeoM465A0FwPB0+gNvdM/xPBRIqNtC4mW+mBKy+ZKF9CWa7rEqv87OODGrigkEDpkH8Bs1FKZYbuHKCQNQ==} 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.56.0': + resolution: {integrity: sha512-2wKd6+/nKyZVTkElTHRZAAEQ7moGqGmTIXlZvfAeV/dNA+6zbbl85JBcyeUFIYt+I42Naq5RgKtUY8fK6/GE1g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-pg@0.61.2': - resolution: {integrity: sha512-l1tN4dX8Ig1bKzMu81Q1EBXWFRy9wqchXbeHDRniJsXYND5dC8u1Uhah7wz1zZta3fbBWflP2mJZcDPWNsAMRg==} + '@opentelemetry/instrumentation-pg@0.62.0': + resolution: {integrity: sha512-/ZSMRCyFRMjQVx7Wf+BIAOMEdN/XWBbAGTNLKfQgGYs1GlmdiIFkUy8Z8XGkToMpKrgZju0drlTQpqt4Ul7R6w==} 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.210.0': + resolution: {integrity: sha512-sLMhyHmW9katVaLUOKpfCnxSGhZq2t1ReWgwsu2cSgxmDVMB690H9TanuexanpFI94PJaokrqbp8u9KYZDUT5g==} 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.210.0': + resolution: {integrity: sha512-uk78DcZoBNHIm26h0oXc8Pizh4KDJ/y04N5k/UaI9J7xR7mL8QcMcYPQG9xxN7m8qotXOMDRW6qTAyptav4+3w==} 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.210.0': + resolution: {integrity: sha512-fEJs8UhkFMrdXMOCLXyKd2uc6N209tIi8IBNqSTi83ri+MlMFrBKnOtklmv9/zzxovoN5zD1waRt6XBFGPfmIw==} 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.210.0': + resolution: {integrity: sha512-nkHBJVSJGOwkRZl+BFIr7gikA93/U8XkL2EWaiDbj3DVjmTEZQpegIKk0lT8oqQYfP8FC6zWNjuTfkaBVqa0ZQ==} 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.4.0': + resolution: {integrity: sha512-6VPsFiMUkJBre/86F0d+PZMaUCcuLA9DtZuC46KH8EeVEKZPEM2WlX35M/qmde8UpzoQL9qzdz54YjUYABt8Uw==} 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.4.0': + resolution: {integrity: sha512-t6muBL/3AMD++1EMF658C/KIpj3gfmTmftX3mEQql4KIxNGFvacCmmTtrQt9IZAJmQRfjQRCkv+vsGbQugeJIw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -3819,38 +4000,38 @@ 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.4.0': + resolution: {integrity: sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==} 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.210.0': + resolution: {integrity: sha512-YuaL92Dpyk/Kc1o4e9XiaWWwiC0aBFN+4oy+6A9TP4UNJmRymPMEX10r6EMMFMD7V0hktiSig9cwWo59peeLCQ==} 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.4.0': + resolution: {integrity: sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw==} 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.210.0': + resolution: {integrity: sha512-KymqUtYvfpblDNgGxBXYqCcDjYXwjOF7Muc6ocs0rMlG/66Hcs9KiJ7hg4zLOv63JubF/vxi5WXaLrQrPKyaZQ==} 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.4.0': + resolution: {integrity: sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==} 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.4.0': + resolution: {integrity: sha512-MBc2l04hZPYygnWPT38UiOPy9ueutPqmJ47z0m9IKuoVQh3MblmbSgwspjhdHagZLfSfmlzhWR1xtbgVNmjX2A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -3950,35 +4131,35 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} - '@photo-sphere-viewer/core@5.14.0': - resolution: {integrity: sha512-V0JeDSB1D2Q60Zqn7+0FPjq8gqbKEwuxMzNdTLydefkQugVztLvdZykO+4k5XTpweZ2QAWPH/QOI1xZbsdvR9A==} + '@photo-sphere-viewer/core@5.14.1': + resolution: {integrity: sha512-qrwUudrX9YZms4c2shlY/H3jUP0oh9FyGEqIDr/95ulNZgKbhQ6C/i8zDQ4j8ooFR4+z5FDORQtGvLgPyX8VCA==} - '@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==} @@ -4182,113 +4363,128 @@ 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] - '@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] - '@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] - '@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] - '@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] - '@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] + + '@rollup/rollup-linux-ppc64-gnu@4.55.1': + resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} cpu: [ppc64] os: [linux] - '@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] + + '@rollup/rollup-linux-riscv64-gnu@4.55.1': + resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] - '@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] - '@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] - '@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] - '@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] - '@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] @@ -4327,32 +4523,32 @@ 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==} + '@smithy/abort-controller@4.2.8': + resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==} engines: {node: '>=18.0.0'} - '@smithy/config-resolver@4.4.4': - resolution: {integrity: sha512-s3U5ChS21DwU54kMmZ0UJumoS5cg0+rGVZvN6f5Lp6EbAVi0ZyP+qDSHdewfmXKUgNK1j3z45JyzulkDukrjAA==} + '@smithy/config-resolver@4.4.6': + resolution: {integrity: sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==} engines: {node: '>=18.0.0'} - '@smithy/core@3.19.0': - resolution: {integrity: sha512-Y9oHXpBcXQgYHOcAEmxjkDilUbSTkgKjoHYed3WaYUH8jngq8lPWDBSpjHblJ9uOgBdy5mh3pzebrScDdYr29w==} + '@smithy/core@3.20.7': + resolution: {integrity: sha512-aO7jmh3CtrmPsIJxUwYIzI5WVlMK8BMCPQ4D4nTzqTqBhbzvxHNzBMGcEg13yg/z9R2Qsz49NUFl0F0lVbTVFw==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.2.6': - resolution: {integrity: sha512-xBmawExyTzOjbhzkZwg+vVm/khg28kG+rj2sbGlULjFd1jI70sv/cbpaR0Ev4Yfd6CpDUDRMe64cTqR//wAOyA==} + '@smithy/credential-provider-imds@4.2.8': + resolution: {integrity: sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.3.7': - resolution: {integrity: sha512-fcVap4QwqmzQwQK9QU3keeEpCzTjnP9NJ171vI7GnD7nbkAIcP9biZhDUx88uRH9BabSsQDS0unUps88uZvFIQ==} + '@smithy/fetch-http-handler@5.3.9': + resolution: {integrity: sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==} engines: {node: '>=18.0.0'} - '@smithy/hash-node@4.2.6': - resolution: {integrity: sha512-k3Dy9VNR37wfMh2/1RHkFf/e0rMyN0pjY0FdyY6ItJRjENYyVPRMwad6ZR1S9HFm6tTuIOd9pqKBmtJ4VHxvxg==} + '@smithy/hash-node@4.2.8': + resolution: {integrity: sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==} engines: {node: '>=18.0.0'} - '@smithy/invalid-dependency@4.2.6': - resolution: {integrity: sha512-E4t/V/q2T46RY21fpfznd1iSLTvCXKNKo4zJ1QuEFN4SE9gKfu2vb6bgq35LpufkQ+SETWIC7ZAf2GGvTlBaMQ==} + '@smithy/invalid-dependency@4.2.8': + resolution: {integrity: sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==} engines: {node: '>=18.0.0'} '@smithy/is-array-buffer@2.2.0': @@ -4363,72 +4559,72 @@ packages: resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} engines: {node: '>=18.0.0'} - '@smithy/middleware-content-length@4.2.6': - resolution: {integrity: sha512-0cjqjyfj+Gls30ntq45SsBtqF3dfJQCeqQPyGz58Pk8OgrAr5YiB7ZvDzjCA94p4r6DCI4qLm7FKobqBjf515w==} + '@smithy/middleware-content-length@4.2.8': + resolution: {integrity: sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.4.0': - resolution: {integrity: sha512-M6qWfUNny6NFNy8amrCGIb9TfOMUkHVtg9bHtEFGRgfH7A7AtPpn/fcrToGPjVDK1ECuMVvqGQOXcZxmu9K+7A==} + '@smithy/middleware-endpoint@4.4.8': + resolution: {integrity: sha512-TV44qwB/T0OMMzjIuI+JeS0ort3bvlPJ8XIH0MSlGADraXpZqmyND27ueuAL3E14optleADWqtd7dUgc2w+qhQ==} engines: {node: '>=18.0.0'} - '@smithy/middleware-retry@4.4.16': - resolution: {integrity: sha512-XPpNhNRzm3vhYm7YCsyw3AtmWggJbg1wNGAoqb7NBYr5XA5isMRv14jgbYyUV6IvbTBFZQdf2QpeW43LrRdStQ==} + '@smithy/middleware-retry@4.4.24': + resolution: {integrity: sha512-yiUY1UvnbUFfP5izoKLtfxDSTRv724YRRwyiC/5HYY6vdsVDcDOXKSXmkJl/Hovcxt5r+8tZEUAdrOaCJwrl9Q==} engines: {node: '>=18.0.0'} - '@smithy/middleware-serde@4.2.7': - resolution: {integrity: sha512-PFMVHVPgtFECeu4iZ+4SX6VOQT0+dIpm4jSPLLL6JLSkp9RohGqKBKD0cbiXdeIFS08Forp0UHI6kc0gIHenSA==} + '@smithy/middleware-serde@4.2.9': + resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==} engines: {node: '>=18.0.0'} - '@smithy/middleware-stack@4.2.6': - resolution: {integrity: sha512-JSbALU3G+JS4kyBZPqnJ3hxIYwOVRV7r9GNQMS6j5VsQDo5+Es5nddLfr9TQlxZLNHPvKSh+XSB0OuWGfSWFcA==} + '@smithy/middleware-stack@4.2.8': + resolution: {integrity: sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==} engines: {node: '>=18.0.0'} - '@smithy/node-config-provider@4.3.6': - resolution: {integrity: sha512-fYEyL59Qe82Ha1p97YQTMEQPJYmBS+ux76foqluaTVWoG9Px5J53w6NvXZNE3wP7lIicLDF7Vj1Em18XTX7fsA==} + '@smithy/node-config-provider@4.3.8': + resolution: {integrity: sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.4.6': - resolution: {integrity: sha512-Gsb9jf4ido5BhPfani4ggyrKDd3ZK+vTFWmUaZeFg5G3E5nhFmqiTzAIbHqmPs1sARuJawDiGMGR/nY+Gw6+aQ==} + '@smithy/node-http-handler@4.4.8': + resolution: {integrity: sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==} engines: {node: '>=18.0.0'} - '@smithy/property-provider@4.2.6': - resolution: {integrity: sha512-a/tGSLPtaia2krbRdwR4xbZKO8lU67DjMk/jfY4QKt4PRlKML+2tL/gmAuhNdFDioO6wOq0sXkfnddNFH9mNUA==} + '@smithy/property-provider@4.2.8': + resolution: {integrity: sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==} engines: {node: '>=18.0.0'} - '@smithy/protocol-http@5.3.6': - resolution: {integrity: sha512-qLRZzP2+PqhE3OSwvY2jpBbP0WKTZ9opTsn+6IWYI0SKVpbG+imcfNxXPq9fj5XeaUTr7odpsNpK6dmoiM1gJQ==} + '@smithy/protocol-http@5.3.8': + resolution: {integrity: sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==} engines: {node: '>=18.0.0'} - '@smithy/querystring-builder@4.2.6': - resolution: {integrity: sha512-MeM9fTAiD3HvoInK/aA8mgJaKQDvm8N0dKy6EiFaCfgpovQr4CaOkJC28XqlSRABM+sHdSQXbC8NZ0DShBMHqg==} + '@smithy/querystring-builder@4.2.8': + resolution: {integrity: sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==} engines: {node: '>=18.0.0'} - '@smithy/querystring-parser@4.2.6': - resolution: {integrity: sha512-YmWxl32SQRw/kIRccSOxzS/Ib8/b5/f9ex0r5PR40jRJg8X1wgM3KrR2In+8zvOGVhRSXgvyQpw9yOSlmfmSnA==} + '@smithy/querystring-parser@4.2.8': + resolution: {integrity: sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==} engines: {node: '>=18.0.0'} - '@smithy/service-error-classification@4.2.6': - resolution: {integrity: sha512-Q73XBrzJlGTut2nf5RglSntHKgAG0+KiTJdO5QQblLfr4TdliGwIAha1iZIjwisc3rA5ulzqwwsYC6xrclxVQg==} + '@smithy/service-error-classification@4.2.8': + resolution: {integrity: sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==} engines: {node: '>=18.0.0'} - '@smithy/shared-ini-file-loader@4.4.1': - resolution: {integrity: sha512-tph+oQYPbpN6NamF030hx1gb5YN2Plog+GLaRHpoEDwp8+ZPG26rIJvStG9hkWzN2HBn3HcWg0sHeB0tmkYzqA==} + '@smithy/shared-ini-file-loader@4.4.3': + resolution: {integrity: sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.3.6': - resolution: {integrity: sha512-P1TXDHuQMadTMTOBv4oElZMURU4uyEhxhHfn+qOc2iofW9Rd4sZtBGx58Lzk112rIGVEYZT8eUMK4NftpewpRA==} + '@smithy/signature-v4@5.3.8': + resolution: {integrity: sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==} engines: {node: '>=18.0.0'} - '@smithy/smithy-client@4.10.1': - resolution: {integrity: sha512-1ovWdxzYprhq+mWqiGZlt3kF69LJthuQcfY9BIyHx9MywTFKzFapluku1QXoaBB43GCsLDxNqS+1v30ure69AA==} + '@smithy/smithy-client@4.10.9': + resolution: {integrity: sha512-Je0EvGXVJ0Vrrr2lsubq43JGRIluJ/hX17aN/W/A0WfE+JpoMdI8kwk2t9F0zTX9232sJDGcoH4zZre6m6f/sg==} engines: {node: '>=18.0.0'} - '@smithy/types@4.10.0': - resolution: {integrity: sha512-K9mY7V/f3Ul+/Gz4LJANZ3vJ/yiBIwCyxe0sPT4vNJK63Srvd+Yk1IzP0t+nE7XFSpIGtzR71yljtnqpUTYFlQ==} + '@smithy/types@4.12.0': + resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==} engines: {node: '>=18.0.0'} - '@smithy/url-parser@4.2.6': - resolution: {integrity: sha512-tVoyzJ2vXp4R3/aeV4EQjBDmCuWxRa8eo3KybL7Xv4wEM16nObYh7H1sNfcuLWHAAAzb0RVyxUz1S3sGj4X+Tg==} + '@smithy/url-parser@4.2.8': + resolution: {integrity: sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==} engines: {node: '>=18.0.0'} '@smithy/util-base64@4.3.0': @@ -4455,32 +4651,32 @@ packages: 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==} + '@smithy/util-defaults-mode-browser@4.3.23': + resolution: {integrity: sha512-mMg+r/qDfjfF/0psMbV4zd7F/i+rpyp7Hjh0Wry7eY15UnzTEId+xmQTGDU8IdZtDfbGQxuWNfgBZKBj+WuYbA==} engines: {node: '>=18.0.0'} - '@smithy/util-defaults-mode-node@4.2.18': - resolution: {integrity: sha512-Kw2J+KzYm9C9Z9nY6+W0tEnoZOofstVCMTshli9jhQbQCy64rueGfKzPfuFBnVUqZD9JobxTh2DzHmPkp/Va/Q==} + '@smithy/util-defaults-mode-node@4.2.26': + resolution: {integrity: sha512-EQqe/WkbCinah0h1lMWh9ICl0Ob4lyl20/10WTB35SC9vDQfD8zWsOT+x2FIOXKAoZQ8z/y0EFMoodbcqWJY/w==} engines: {node: '>=18.0.0'} - '@smithy/util-endpoints@3.2.6': - resolution: {integrity: sha512-v60VNM2+mPvgHCBXEfMCYrQ0RepP6u6xvbAkMenfe4Mi872CqNkJzgcnQL837e8NdeDxBgrWQRTluKq5Lqdhfg==} + '@smithy/util-endpoints@3.2.8': + resolution: {integrity: sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==} 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==} + '@smithy/util-middleware@4.2.8': + resolution: {integrity: sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==} engines: {node: '>=18.0.0'} - '@smithy/util-retry@4.2.6': - resolution: {integrity: sha512-x7CeDQLPQ9cb6xN7fRJEjlP9NyGW/YeXWc4j/RUhg4I+H60F0PEeRc2c/z3rm9zmsdiMFzpV/rT+4UHW6KM1SA==} + '@smithy/util-retry@4.2.8': + resolution: {integrity: sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==} engines: {node: '>=18.0.0'} - '@smithy/util-stream@4.5.7': - resolution: {integrity: sha512-Uuy4S5Aj4oF6k1z+i2OtIBJUns4mlg29Ph4S+CqjR+f4XXpSFVgTCYLzMszHJTicYDBxKFtwq2/QSEDSS5l02A==} + '@smithy/util-stream@4.5.10': + resolution: {integrity: sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==} engines: {node: '>=18.0.0'} '@smithy/util-uri-escape@4.2.0': @@ -4531,18 +4727,21 @@ packages: 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.49.5': + resolution: {integrity: sha512-dCYqelr2RVnWUuxc+Dk/dB/SjV/8JBndp1UovCyCZdIQezd8TRwFLNZctYkzgHxRJtaNvseCSRsuuHPeUgIN/A==} 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 svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.3.3 vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 peerDependenciesMeta: '@opentelemetry/api': optional: true + typescript: + optional: true '@sveltejs/vite-plugin-svelte-inspector@5.0.1': resolution: {integrity: sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==} @@ -4552,8 +4751,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 @@ -4857,14 +5056,14 @@ packages: 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==} @@ -4934,6 +5133,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==} @@ -5057,8 +5349,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==} @@ -5096,17 +5388,17 @@ packages: '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@20.19.27': - resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} + '@types/node@20.19.30': + resolution: {integrity: sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==} - '@types/node@24.10.4': - resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} + '@types/node@24.10.9': + resolution: {integrity: sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==} - '@types/node@25.0.3': - resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + '@types/node@25.0.9': + resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==} - '@types/nodemailer@7.0.4': - resolution: {integrity: sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==} + '@types/nodemailer@7.0.5': + resolution: {integrity: sha512-7WtR4MFJUNN2UFy0NIowBRJswj5KXjXDhlZY43Hmots5eGu5q/dTeFd/I6GgJA/qj3RqO6dDy4SvfcV3fOVeIA==} '@types/oidc-provider@9.5.0': resolution: {integrity: sha512-eEzCRVTSqIHD9Bo/qRJ4XQWQ5Z/zBcG+Z2cGJluRsSuWx1RJihqRyPxhIEpMXTwPzHYRTQkVp7hwisQOwzzSAg==} @@ -5114,8 +5406,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==} @@ -5150,8 +5442,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.8': + resolution: {integrity: sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==} '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} @@ -5207,6 +5499,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==} @@ -5231,63 +5526,63 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.51.0': - resolution: {integrity: sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==} + '@typescript-eslint/eslint-plugin@8.53.0': + resolution: {integrity: sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.51.0 + '@typescript-eslint/parser': ^8.53.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.51.0': - resolution: {integrity: sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==} + '@typescript-eslint/parser@8.53.0': + resolution: {integrity: sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.51.0': - resolution: {integrity: sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==} + '@typescript-eslint/project-service@8.53.0': + resolution: {integrity: sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.51.0': - resolution: {integrity: sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==} + '@typescript-eslint/scope-manager@8.53.0': + resolution: {integrity: sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.51.0': - resolution: {integrity: sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==} + '@typescript-eslint/tsconfig-utils@8.53.0': + resolution: {integrity: sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.51.0': - resolution: {integrity: sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==} + '@typescript-eslint/type-utils@8.53.0': + resolution: {integrity: sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.51.0': - resolution: {integrity: sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==} + '@typescript-eslint/types@8.53.0': + resolution: {integrity: sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.51.0': - resolution: {integrity: sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==} + '@typescript-eslint/typescript-estree@8.53.0': + resolution: {integrity: sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.51.0': - resolution: {integrity: sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==} + '@typescript-eslint/utils@8.53.0': + resolution: {integrity: sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.51.0': - resolution: {integrity: sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==} + '@typescript-eslint/visitor-keys@8.53.0': + resolution: {integrity: sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -5778,8 +6073,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: @@ -5838,8 +6133,8 @@ packages: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} - bullmq@5.66.4: - resolution: {integrity: sha512-y2VRk2z7d1YNI2JQDD7iThoD0X/0iZZ3VEp8lqT5s5U0XDl9CIjXp1LQgmE9EKy6ReHtzmYXS1f328PnUbZGtQ==} + bullmq@5.66.5: + resolution: {integrity: sha512-DC1E7P03L+TfNHv+2SGxwNYvtb0oJPODWSKkWdfis0heU5zFW16vjM7fCjwlxMdGWw2w28EI3mTRfYLEHeQQSw==} bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} @@ -5979,6 +6274,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'} @@ -6198,6 +6501,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==} @@ -6293,6 +6599,12 @@ packages: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} 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'} @@ -6475,18 +6787,166 @@ packages: 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'} + dagre-d3-es@7.0.13: + resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} + data-urls@3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} @@ -6495,6 +6955,9 @@ packages: 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==} @@ -6595,6 +7058,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'} @@ -6642,8 +7108,8 @@ packages: engines: {node: '>= 4.0.0'} hasBin: true - devalue@5.6.1: - resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==} + devalue@5.6.2: + resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -6736,6 +7202,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==} @@ -6854,6 +7323,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'} @@ -6937,8 +7409,8 @@ packages: peerDependencies: eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.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' @@ -6951,8 +7423,8 @@ 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.14.0: + resolution: {integrity: sha512-Isw0GvaMm0yHxAj71edAdGFh28ufYs+6rk2KlbbZphnqZAzrH3Se3t12IFh2H9+1F/jlDhBBL4oiOJmLqmYX0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.1 || ^9.0.0 @@ -7224,8 +7696,8 @@ packages: file-source@0.6.1: resolution: {integrity: sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==} - file-type@21.2.0: - resolution: {integrity: sha512-vCYBgFOrJQLoTzDyAXAL/RFfKnXXpUYt4+tipVy26nJJhT7ftgGETf2tAQF59EEL61i3MrorV/PG6tf7LJK7eg==} + file-type@21.3.0: + resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} engines: {node: '>=20'} fill-range@7.1.1: @@ -7380,8 +7852,8 @@ 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: @@ -7523,6 +7995,9 @@ packages: 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==} @@ -7531,8 +8006,8 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - happy-dom@20.0.11: - resolution: {integrity: sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==} + happy-dom@20.3.0: + resolution: {integrity: sha512-5qJbkqcvR8j/a4av5IWqqIWmEGf9dt6OhGMS6qxCgjSOBGzGa5XLoqg40OyD8XNzQ+g1g2zsXi10kjfpzYH55Q==} engines: {node: '>=20.0.0'} has-flag@4.0.0: @@ -7761,8 +8236,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: @@ -7851,6 +8326,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'} @@ -7858,14 +8336,14 @@ packages: intl-messageformat@10.7.18: resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} - intl-messageformat@11.0.8: - resolution: {integrity: sha512-q2Md8nj28CSkXxkBaAOWhTjQAdea24fpcZxqR1pMsCwzDYLQF68iOOPNTLgFFF+HKJKNUiJ+Mkjp0zXvG88UFA==} + intl-messageformat@11.0.9: + resolution: {integrity: sha512-xA4aCCMnCxynKV5kI7V0GlMf+BGJxsXQRwr5tfEgmcB791eDEQa4r+s4wU7GqMR0jx7+K4jyEH2UfBpVGTDNPQ==} 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.1: + resolution: {integrity: sha512-BXNqFQ66oOsR82g9ajFFsR8ZKrjVvYCLyeML9IvSMAsP56XH2VXBdZjmI11p65nXXJxTEt1hie3J2QeFJVgrtQ==} engines: {node: '>=12.22.0'} ip-address@10.1.0: @@ -8234,6 +8712,10 @@ packages: jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + katex@0.16.27: + resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==} + hasBin: true + kdbush@3.0.0: resolution: {integrity: sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==} @@ -8248,6 +8730,9 @@ packages: 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'} @@ -8284,6 +8769,10 @@ packages: 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'} @@ -8291,6 +8780,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'} @@ -8421,8 +8916,11 @@ packages: resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lodash-es@4.17.22: - resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==} + 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==} @@ -8469,6 +8967,9 @@ packages: 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==} engines: {node: '>=10'} @@ -8553,8 +9054,8 @@ packages: resolution: {integrity: sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==} engines: {node: '>=6.4.0'} - maplibre-gl@5.15.0: - resolution: {integrity: sha512-pPeu/t4yPDX/+Uf9ibLUdmaKbNMlGxMAX+tBednYukol2qNk2TZXAlhdohWxjVvTO3is8crrUYv3Ok02oAaKzA==} + maplibre-gl@5.16.0: + resolution: {integrity: sha512-/VDY89nr4jgLJyzmhy325cG6VUI02WkZ/UfVuDbG/piXzo6ODnM+omDFIwWY8tsEsBG26DNDmNMn3Y2ikHsBiA==} engines: {node: '>=16.14.0', npm: '>=8.1.0'} mark.js@8.11.1: @@ -8680,6 +9181,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'} @@ -8952,6 +9456,9 @@ packages: 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==} @@ -9004,6 +9511,10 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -9220,6 +9731,9 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + oidc-provider@9.6.0: resolution: {integrity: sha512-CCRUYPOumEy/DT+L86H40WgXjXfDHlsJYZdyd4ZKGFxJh/kAd7DxMX3dwpbX0g+WjB+NWU+kla1b/yZmHNcR0Q==} @@ -9332,6 +9846,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==} @@ -9371,6 +9888,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'} @@ -9438,30 +9958,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.10.0: + resolution: {integrity: sha512-ur/eoPKzDx2IjPaYyXS6Y8NSblxM7X64deV2ObV57vhjsWiwLvUD6meukAzogiOsu60GO8m/3Cb6FdJsWNjwXg==} 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.17.1: + resolution: {integrity: sha512-EIR+jXdYNSMOrpRp7g6WgQr7SaZNZfS7IzZIO0oTNEeibq956JxeD15t3Jk3zZH0KH8DmOIx38qJfQenoE8bXQ==} engines: {node: '>= 16.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -9499,6 +10019,9 @@ 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==} @@ -9533,6 +10056,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'} @@ -9988,8 +10517,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: @@ -10000,8 +10529,8 @@ 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: @@ -10014,8 +10543,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: @@ -10028,8 +10557,8 @@ 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 @@ -10040,8 +10569,8 @@ packages: 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.0: + resolution: {integrity: sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==} engines: {node: '>=14'} hasBin: true @@ -10107,6 +10636,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==} @@ -10136,8 +10669,8 @@ 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: @@ -10484,11 +11017,14 @@ 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'} @@ -10827,8 +11363,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.0: + resolution: {integrity: sha512-o2yiy7fYXK1HvzA8P6wwj8QSuwG3e/XcpWht/jIxkQX99c0SVPw0OXdLSV9fHASPiYB09HLA0uq8hokGydi/QA==} hasBin: true srcset@4.0.0: @@ -10963,13 +11499,16 @@ 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: @@ -10978,8 +11517,8 @@ packages: 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: @@ -11072,8 +11611,8 @@ packages: peerDependencies: svelte: ^5.30.2 - svelte@5.46.1: - resolution: {integrity: sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA==} + svelte@5.48.0: + resolution: {integrity: sha512-+NUe82VoFP1RQViZI/esojx70eazGF4u0O/9ucqZ4rPcOZD+n5EVp17uYsqwdzjUjZyTpGKunHbDziW6AIAVkQ==} engines: {node: '>=18'} svg-parser@2.0.4: @@ -11084,8 +11623,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==} @@ -11099,8 +11638,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: @@ -11109,8 +11648,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==} @@ -11170,10 +11709,12 @@ 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 exhorbitant rates) by contacting i@izs.me tar@7.5.2: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} 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 exhorbitant rates) by contacting i@izs.me terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} @@ -11261,6 +11802,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'} @@ -11332,6 +11877,9 @@ packages: 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==} engines: {node: '>=10.0'} @@ -11356,6 +11904,10 @@ packages: 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==} @@ -11425,8 +11977,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.51.0: - resolution: {integrity: sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==} + typescript-eslint@8.53.0: + resolution: {integrity: sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -11440,10 +11992,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.8: + resolution: {integrity: sha512-BdnBM5waFormdrOFBU+cA90R689V0tWUWlIG2i30UXxElHjuCu5+dOV2Etw3547jcQ/yaLtPm9wrqIuOY2bSJg==} 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'} @@ -11681,16 +12236,16 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite-tsconfig-paths@6.0.3: - resolution: {integrity: sha512-7bL7FPX/DSviaZGYUKowWF1AiDVWjMjxNbE8lyaVGDezkedWqfGhlnQ4BZXre0ZN5P4kAgIJfAlgFDVyjrCIyg==} + vite-tsconfig-paths@6.0.4: + resolution: {integrity: sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ==} 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: @@ -11771,6 +12326,26 @@ packages: jsdom: optional: true + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + 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==} + vt-pbf@3.1.3: resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==} @@ -11785,8 +12360,8 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} - watchpack@2.4.4: - resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} wbuf@1.7.3: @@ -11854,8 +12429,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: @@ -11958,6 +12533,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -11988,6 +12567,18 @@ packages: utf-8-validate: optional: true + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} @@ -12262,11 +12853,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.10.9)(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.10.9) ansi-colors: 4.1.3 symbol-observable: 4.0.0 yargs-parser: 21.1.1 @@ -12294,6 +12885,11 @@ snapshots: transitivePeerDependencies: - chokidar + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -12308,15 +12904,15 @@ snapshots: '@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 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-locate-window': 3.965.2 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.936.0 + '@aws-sdk/types': 3.969.0 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -12325,383 +12921,383 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.936.0 + '@aws-sdk/types': 3.969.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-sesv2@3.952.0': + '@aws-sdk/client-sesv2@3.971.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 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/credential-provider-node': 3.971.0 + '@aws-sdk/middleware-host-header': 3.969.0 + '@aws-sdk/middleware-logger': 3.969.0 + '@aws-sdk/middleware-recursion-detection': 3.969.0 + '@aws-sdk/middleware-user-agent': 3.970.0 + '@aws-sdk/region-config-resolver': 3.969.0 + '@aws-sdk/signature-v4-multi-region': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-endpoints': 3.970.0 + '@aws-sdk/util-user-agent-browser': 3.969.0 + '@aws-sdk/util-user-agent-node': 3.971.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.20.7 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.8 + '@smithy/middleware-retry': 4.4.24 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 '@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-defaults-mode-browser': 4.3.23 + '@smithy/util-defaults-mode-node': 4.2.26 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso@3.948.0': + '@aws-sdk/client-sso@3.971.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 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/middleware-host-header': 3.969.0 + '@aws-sdk/middleware-logger': 3.969.0 + '@aws-sdk/middleware-recursion-detection': 3.969.0 + '@aws-sdk/middleware-user-agent': 3.970.0 + '@aws-sdk/region-config-resolver': 3.969.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-endpoints': 3.970.0 + '@aws-sdk/util-user-agent-browser': 3.969.0 + '@aws-sdk/util-user-agent-node': 3.971.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.20.7 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.8 + '@smithy/middleware-retry': 4.4.24 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 '@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-defaults-mode-browser': 4.3.23 + '@smithy/util-defaults-mode-node': 4.2.26 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.947.0': + '@aws-sdk/core@3.970.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 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/xml-builder': 3.969.0 + '@smithy/core': 3.20.7 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 '@smithy/util-base64': 4.3.0 - '@smithy/util-middleware': 4.2.6 + '@smithy/util-middleware': 4.2.8 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.947.0': + '@aws-sdk/credential-provider-env@3.970.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 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.947.0': + '@aws-sdk/credential-provider-http@3.970.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 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.8 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.10 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.952.0': + '@aws-sdk/credential-provider-ini@3.971.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 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/credential-provider-env': 3.970.0 + '@aws-sdk/credential-provider-http': 3.970.0 + '@aws-sdk/credential-provider-login': 3.971.0 + '@aws-sdk/credential-provider-process': 3.970.0 + '@aws-sdk/credential-provider-sso': 3.971.0 + '@aws-sdk/credential-provider-web-identity': 3.971.0 + '@aws-sdk/nested-clients': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-login@3.952.0': + '@aws-sdk/credential-provider-login@3.971.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 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/nested-clients': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.952.0': + '@aws-sdk/credential-provider-node@3.971.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 + '@aws-sdk/credential-provider-env': 3.970.0 + '@aws-sdk/credential-provider-http': 3.970.0 + '@aws-sdk/credential-provider-ini': 3.971.0 + '@aws-sdk/credential-provider-process': 3.970.0 + '@aws-sdk/credential-provider-sso': 3.971.0 + '@aws-sdk/credential-provider-web-identity': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-process@3.947.0': + '@aws-sdk/credential-provider-process@3.970.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 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.952.0': + '@aws-sdk/credential-provider-sso@3.971.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 + '@aws-sdk/client-sso': 3.971.0 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/token-providers': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-web-identity@3.952.0': + '@aws-sdk/credential-provider-web-identity@3.971.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 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/nested-clients': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/middleware-host-header@3.936.0': + '@aws-sdk/middleware-host-header@3.969.0': dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 + '@aws-sdk/types': 3.969.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-logger@3.936.0': + '@aws-sdk/middleware-logger@3.969.0': dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/types': 4.10.0 + '@aws-sdk/types': 3.969.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.948.0': + '@aws-sdk/middleware-recursion-detection@3.969.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 + '@aws-sdk/types': 3.969.0 + '@aws/lambda-invoke-store': 0.2.3 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.947.0': + '@aws-sdk/middleware-sdk-s3@3.970.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 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-arn-parser': 3.968.0 + '@smithy/core': 3.20.7 + '@smithy/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 '@smithy/util-config-provider': 4.2.0 - '@smithy/util-middleware': 4.2.6 - '@smithy/util-stream': 4.5.7 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-stream': 4.5.10 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.947.0': + '@aws-sdk/middleware-user-agent@3.970.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 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-endpoints': 3.970.0 + '@smithy/core': 3.20.7 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.952.0': + '@aws-sdk/nested-clients@3.971.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 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/middleware-host-header': 3.969.0 + '@aws-sdk/middleware-logger': 3.969.0 + '@aws-sdk/middleware-recursion-detection': 3.969.0 + '@aws-sdk/middleware-user-agent': 3.970.0 + '@aws-sdk/region-config-resolver': 3.969.0 + '@aws-sdk/types': 3.969.0 + '@aws-sdk/util-endpoints': 3.970.0 + '@aws-sdk/util-user-agent-browser': 3.969.0 + '@aws-sdk/util-user-agent-node': 3.971.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/core': 3.20.7 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/hash-node': 4.2.8 + '@smithy/invalid-dependency': 4.2.8 + '@smithy/middleware-content-length': 4.2.8 + '@smithy/middleware-endpoint': 4.4.8 + '@smithy/middleware-retry': 4.4.24 + '@smithy/middleware-serde': 4.2.9 + '@smithy/middleware-stack': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/node-http-handler': 4.4.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 '@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-defaults-mode-browser': 4.3.23 + '@smithy/util-defaults-mode-node': 4.2.26 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/region-config-resolver@3.936.0': + '@aws-sdk/region-config-resolver@3.969.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 + '@aws-sdk/types': 3.969.0 + '@smithy/config-resolver': 4.4.6 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.947.0': + '@aws-sdk/signature-v4-multi-region@3.970.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 + '@aws-sdk/middleware-sdk-s3': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/signature-v4': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.952.0': + '@aws-sdk/token-providers@3.971.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 + '@aws-sdk/core': 3.970.0 + '@aws-sdk/nested-clients': 3.971.0 + '@aws-sdk/types': 3.969.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 transitivePeerDependencies: - aws-crt - '@aws-sdk/types@3.936.0': + '@aws-sdk/types@3.969.0': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/util-arn-parser@3.893.0': + '@aws-sdk/util-arn-parser@3.968.0': dependencies: tslib: 2.8.1 - '@aws-sdk/util-endpoints@3.936.0': + '@aws-sdk/util-endpoints@3.970.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 + '@aws-sdk/types': 3.969.0 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-endpoints': 3.2.8 tslib: 2.8.1 - '@aws-sdk/util-locate-window@3.893.0': + '@aws-sdk/util-locate-window@3.965.2': dependencies: tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.936.0': + '@aws-sdk/util-user-agent-browser@3.969.0': dependencies: - '@aws-sdk/types': 3.936.0 - '@smithy/types': 4.10.0 + '@aws-sdk/types': 3.969.0 + '@smithy/types': 4.12.0 bowser: 2.13.1 tslib: 2.8.1 - '@aws-sdk/util-user-agent-node@3.947.0': + '@aws-sdk/util-user-agent-node@3.971.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 + '@aws-sdk/middleware-user-agent': 3.970.0 + '@aws-sdk/types': 3.969.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.930.0': + '@aws-sdk/xml-builder@3.969.0': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 fast-xml-parser: 5.2.5 tslib: 2.8.1 - '@aws/lambda-invoke-store@0.2.2': {} + '@aws/lambda-invoke-store@0.2.3': {} - '@babel/code-frame@7.27.1': + '@babel/code-frame@7.28.6': dependencies: '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 @@ -12711,7 +13307,7 @@ snapshots: '@babel/core@7.28.5': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.28.6 '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) @@ -13429,17 +14025,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.28.6 '@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.28.6 '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.5 @@ -13460,6 +14056,25 @@ snapshots: '@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 @@ -13814,26 +14429,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.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': optionalDependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 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.8)(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.8)(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.8 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) search-insights: 2.17.3 @@ -13849,7 +14464,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 @@ -13874,24 +14489,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' @@ -13907,7 +14522,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.8)(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) @@ -13916,7 +14531,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.8)(react@18.3.1) boxen: 6.2.1 chalk: 4.1.2 chokidar: 3.6.0 @@ -13931,9 +14546,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 @@ -13941,7 +14556,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' - react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.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) @@ -13950,9 +14565,9 @@ snapshots: 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' @@ -13992,7 +14607,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 @@ -14008,9 +14623,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 @@ -14022,7 +14637,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.8 '@types/react-router-config': 5.0.11 '@types/react-router-dom': 5.3.3 react: 18.3.1 @@ -14036,13 +14651,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.8)(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.8)(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.8)(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.8)(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.8)(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) @@ -14050,7 +14665,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 @@ -14058,7 +14673,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' @@ -14077,13 +14692,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.8)(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.8)(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.8)(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) @@ -14092,13 +14707,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' @@ -14117,9 +14732,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.8)(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.8)(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) @@ -14128,7 +14743,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' @@ -14147,9 +14762,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.8)(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.8)(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) @@ -14174,9 +14789,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.8)(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.8)(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 @@ -14202,9 +14817,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.8)(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.8)(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 @@ -14228,9 +14843,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.8)(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.8)(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 @@ -14255,9 +14870,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.8)(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.8)(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 @@ -14281,9 +14896,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.8)(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.8)(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) @@ -14312,9 +14927,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.8)(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.8)(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) @@ -14323,7 +14938,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' @@ -14342,22 +14957,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.8)(react@18.3.1))(@types/react@19.2.8)(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.8)(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.8)(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.8)(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.8)(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.8)(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.8)(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.8)(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.8)(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.8)(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.8)(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.8)(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.8)(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.8)(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.8)(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.8)(react@18.3.1))(@types/react@19.2.8)(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) @@ -14384,28 +14999,28 @@ snapshots: '@docusaurus/react-loadable@6.0.0(react@18.3.1)': dependencies: - '@types/react': 19.2.7 + '@types/react': 19.2.8 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.8)(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.8)(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.8)(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.8)(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.8)(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.8)(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.8)(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.8)(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) @@ -14434,15 +15049,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.8)(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.8)(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.8 '@types/react-router-config': 5.0.11 clsx: 2.1.1 parse-numeric-range: 1.3.0 @@ -14458,13 +15073,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.8)(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.8)(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.8)(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.8)(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.8)(react@18.3.1))(@types/react@19.2.8)(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.8)(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.8)(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.8)(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.8)(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) @@ -14473,7 +15118,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 @@ -14511,14 +15156,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.8 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' @@ -14548,7 +15193,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' @@ -14566,22 +15211,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 @@ -14873,12 +15518,12 @@ snapshots: dependencies: urlpattern-polyfill: 8.0.2 - '@faker-js/faker@10.1.0': {} + '@faker-js/faker@10.2.0': {} '@fig/complete-commander@3.2.0(commander@11.1.0)': dependencies: commander: 11.1.0 - prettier: 3.7.4 + prettier: 3.8.0 '@floating-ui/core@1.7.3': dependencies: @@ -14898,10 +15543,10 @@ snapshots: decimal.js: 10.6.0 tslib: 2.8.1 - '@formatjs/ecma402-abstract@3.0.7': + '@formatjs/ecma402-abstract@3.0.8': dependencies: - '@formatjs/fast-memoize': 3.0.2 - '@formatjs/intl-localematcher': 0.7.4 + '@formatjs/fast-memoize': 3.0.3 + '@formatjs/intl-localematcher': 0.7.5 decimal.js: 10.6.0 tslib: 2.8.1 @@ -14909,7 +15554,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@formatjs/fast-memoize@3.0.2': + '@formatjs/fast-memoize@3.0.3': dependencies: tslib: 2.8.1 @@ -14919,10 +15564,10 @@ snapshots: '@formatjs/icu-skeleton-parser': 1.8.16 tslib: 2.8.1 - '@formatjs/icu-messageformat-parser@3.2.1': + '@formatjs/icu-messageformat-parser@3.3.0': dependencies: - '@formatjs/ecma402-abstract': 3.0.7 - '@formatjs/icu-skeleton-parser': 2.0.7 + '@formatjs/ecma402-abstract': 3.0.8 + '@formatjs/icu-skeleton-parser': 2.0.8 tslib: 2.8.1 '@formatjs/icu-skeleton-parser@1.8.16': @@ -14930,18 +15575,18 @@ snapshots: '@formatjs/ecma402-abstract': 2.3.6 tslib: 2.8.1 - '@formatjs/icu-skeleton-parser@2.0.7': + '@formatjs/icu-skeleton-parser@2.0.8': dependencies: - '@formatjs/ecma402-abstract': 3.0.7 + '@formatjs/ecma402-abstract': 3.0.8 tslib: 2.8.1 '@formatjs/intl-localematcher@0.6.2': dependencies: tslib: 2.8.1 - '@formatjs/intl-localematcher@0.7.4': + '@formatjs/intl-localematcher@0.7.5': dependencies: - '@formatjs/fast-memoize': 3.0.2 + '@formatjs/fast-memoize': 3.0.3 tslib: 2.8.1 '@fortawesome/fontawesome-common-types@7.1.0': {} @@ -14954,11 +15599,11 @@ snapshots: dependencies: '@fortawesome/fontawesome-common-types': 7.1.0 - '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)': + '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) - lodash: 4.17.21 + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + lodash: 4.17.23 '@grpc/grpc-js@1.14.3': dependencies: @@ -14996,6 +15641,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': @@ -15094,19 +15747,19 @@ snapshots: '@immich/justified-layout-wasm@0.4.3': {} - '@immich/svelte-markdown-preprocess@0.1.0(svelte@5.46.1)': + '@immich/svelte-markdown-preprocess@0.1.0(svelte@5.48.0)': dependencies: - svelte: 5.46.1 + svelte: 5.48.0 - '@immich/ui@0.54.0(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)': + '@immich/ui@0.59.0(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)': dependencies: - '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.46.1) + '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.48.0) '@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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1) + bits-ui: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0) luxon: 3.7.2 simple-icons: 16.4.0 - svelte: 5.46.1 + svelte: 5.48.0 svelte-highlight: 7.9.0 tailwind-merge: 3.4.0 tailwind-variants: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18) @@ -15116,149 +15769,253 @@ snapshots: '@inquirer/ansi@1.0.2': {} - '@inquirer/checkbox@4.3.2(@types/node@24.10.4)': + '@inquirer/ansi@2.0.3': {} + + '@inquirer/checkbox@4.3.2(@types/node@24.10.9)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/core': 10.3.2(@types/node@24.10.9) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.10.9) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 - '@inquirer/confirm@5.1.21(@types/node@24.10.4)': + '@inquirer/checkbox@5.0.4(@types/node@24.10.9)': dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.4) - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/ansi': 2.0.3 + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/figures': 2.0.3 + '@inquirer/type': 4.0.3(@types/node@24.10.9) optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 - '@inquirer/core@10.3.2(@types/node@24.10.4)': + '@inquirer/confirm@5.1.21(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.9) + '@inquirer/type': 3.0.10(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/confirm@6.0.4(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/core@10.3.2(@types/node@24.10.9)': 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.10.9) 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.10.9 - '@inquirer/editor@4.2.23(@types/node@24.10.4)': + '@inquirer/core@11.1.1(@types/node@24.10.9)': 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/ansi': 2.0.3 + '@inquirer/figures': 2.0.3 + '@inquirer/type': 4.0.3(@types/node@24.10.9) + cli-width: 4.1.0 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + wrap-ansi: 9.0.2 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 - '@inquirer/expand@4.0.23(@types/node@24.10.4)': + '@inquirer/editor@4.2.23(@types/node@24.10.9)': 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.10.9) + '@inquirer/external-editor': 1.0.3(@types/node@24.10.9) + '@inquirer/type': 3.0.10(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/editor@5.0.4(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/external-editor': 2.0.3(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/expand@4.0.23(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.9) + '@inquirer/type': 3.0.10(@types/node@24.10.9) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 - '@inquirer/external-editor@1.0.3(@types/node@24.10.4)': + '@inquirer/expand@5.0.4(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/external-editor@1.0.3(@types/node@24.10.9)': dependencies: chardet: 2.1.1 - iconv-lite: 0.7.1 + iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 + + '@inquirer/external-editor@2.0.3(@types/node@24.10.9)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 24.10.9 '@inquirer/figures@1.0.15': {} - '@inquirer/input@4.3.1(@types/node@24.10.4)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.4) - '@inquirer/type': 3.0.10(@types/node@24.10.4) - optionalDependencies: - '@types/node': 24.10.4 + '@inquirer/figures@2.0.3': {} - '@inquirer/number@3.0.23(@types/node@24.10.4)': + '@inquirer/input@4.3.1(@types/node@24.10.9)': 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.10.9) + '@inquirer/type': 3.0.10(@types/node@24.10.9) optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 - '@inquirer/password@4.0.23(@types/node@24.10.4)': + '@inquirer/input@5.0.4(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/number@3.0.23(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.9) + '@inquirer/type': 3.0.10(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/number@4.0.4(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/password@4.0.23(@types/node@24.10.9)': 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.10.9) + '@inquirer/type': 3.0.10(@types/node@24.10.9) optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 - '@inquirer/prompts@7.10.1(@types/node@24.10.4)': + '@inquirer/password@5.0.4(@types/node@24.10.9)': 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/ansi': 2.0.3 + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 - '@inquirer/prompts@7.3.2(@types/node@24.10.4)': + '@inquirer/prompts@7.3.2(@types/node@24.10.9)': 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.10.9) + '@inquirer/confirm': 5.1.21(@types/node@24.10.9) + '@inquirer/editor': 4.2.23(@types/node@24.10.9) + '@inquirer/expand': 4.0.23(@types/node@24.10.9) + '@inquirer/input': 4.3.1(@types/node@24.10.9) + '@inquirer/number': 3.0.23(@types/node@24.10.9) + '@inquirer/password': 4.0.23(@types/node@24.10.9) + '@inquirer/rawlist': 4.1.11(@types/node@24.10.9) + '@inquirer/search': 3.2.2(@types/node@24.10.9) + '@inquirer/select': 4.4.2(@types/node@24.10.9) optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 - '@inquirer/rawlist@4.1.11(@types/node@24.10.4)': + '@inquirer/prompts@8.2.0(@types/node@24.10.9)': dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.4) - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/checkbox': 5.0.4(@types/node@24.10.9) + '@inquirer/confirm': 6.0.4(@types/node@24.10.9) + '@inquirer/editor': 5.0.4(@types/node@24.10.9) + '@inquirer/expand': 5.0.4(@types/node@24.10.9) + '@inquirer/input': 5.0.4(@types/node@24.10.9) + '@inquirer/number': 4.0.4(@types/node@24.10.9) + '@inquirer/password': 5.0.4(@types/node@24.10.9) + '@inquirer/rawlist': 5.2.0(@types/node@24.10.9) + '@inquirer/search': 4.1.0(@types/node@24.10.9) + '@inquirer/select': 5.0.4(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/rawlist@4.1.11(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.9) + '@inquirer/type': 3.0.10(@types/node@24.10.9) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 - '@inquirer/search@3.2.2(@types/node@24.10.4)': + '@inquirer/rawlist@5.2.0(@types/node@24.10.9)': dependencies: - '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/search@3.2.2(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.10.9) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.10.9) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 - '@inquirer/select@4.4.2(@types/node@24.10.4)': + '@inquirer/search@4.1.0(@types/node@24.10.9)': + dependencies: + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/figures': 2.0.3 + '@inquirer/type': 4.0.3(@types/node@24.10.9) + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/select@4.4.2(@types/node@24.10.9)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@24.10.4) + '@inquirer/core': 10.3.2(@types/node@24.10.9) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@24.10.4) + '@inquirer/type': 3.0.10(@types/node@24.10.9) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 - '@inquirer/type@3.0.10(@types/node@24.10.4)': + '@inquirer/select@5.0.4(@types/node@24.10.9)': + dependencies: + '@inquirer/ansi': 2.0.3 + '@inquirer/core': 11.1.1(@types/node@24.10.9) + '@inquirer/figures': 2.0.3 + '@inquirer/type': 4.0.3(@types/node@24.10.9) optionalDependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 + + '@inquirer/type@3.0.10(@types/node@24.10.9)': + optionalDependencies: + '@types/node': 24.10.9 + + '@inquirer/type@4.0.3(@types/node@24.10.9)': + optionalDependencies: + '@types/node': 24.10.9 '@internationalized/date@3.10.0': dependencies: '@swc/helpers': 0.5.17 - '@ioredis/commands@1.4.0': {} + '@ioredis/commands@1.5.0': {} '@isaacs/balanced-match@4.0.1': {} @@ -15290,7 +16047,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.10.9 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -15383,8 +16140,8 @@ snapshots: '@koddsson/eslint-plugin-tscompat@0.2.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@mdn/browser-compat-data': 6.1.5 - '@typescript-eslint/type-utils': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) browserslist: 4.28.1 transitivePeerDependencies: - eslint @@ -15549,12 +16306,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.8)(react@18.3.1)': dependencies: '@types/mdx': 2.0.13 - '@types/react': 19.2.7 + '@types/react': 19.2.8 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': @@ -15577,39 +16338,39 @@ snapshots: '@namnode/store@0.1.0': {} - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)': + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(bullmq@5.66.4)': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(bullmq@5.66.5)': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) - bullmq: 5.66.4 + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + bullmq: 5.66.5 tslib: 2.8.1 - '@nestjs/cli@11.0.14(@swc/core@1.15.8(@swc/helpers@0.5.17))(@types/node@24.10.4)': + '@nestjs/cli@11.0.15(@swc/core@1.15.8(@swc/helpers@0.5.17))(@types/node@24.10.9)': 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.10.9)(chokidar@4.0.3) + '@inquirer/prompts': 8.2.0(@types/node@24.10.9) '@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.8(@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.8(@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.8(@swc/helpers@0.5.17)) + webpack: 5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17)) webpack-node-externals: 3.0.0 optionalDependencies: '@swc/core': 1.15.8(@swc/helpers@0.5.17) @@ -15619,9 +16380,9 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - file-type: 21.2.0 + file-type: 21.3.0 iterare: 1.2.1 load-esm: 1.0.3 reflect-metadata: 0.2.2 @@ -15634,9 +16395,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/core@11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(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 @@ -15646,21 +16407,21 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) - '@nestjs/websockets': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) + '@nestjs/websockets': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-socket.io@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.11(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.12(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.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(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.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)': + '@nestjs/platform-express@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.5 express: 5.2.1 multer: 2.0.2 @@ -15669,10 +16430,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/platform-socket.io@11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.11)(rxjs@7.8.2)': + '@nestjs/platform-socket.io@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.12)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/websockets': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-socket.io@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) rxjs: 7.8.2 socket.io: 4.8.3 tslib: 2.8.1 @@ -15681,10 +16442,10 @@ snapshots: - supports-color - utf-8-validate - '@nestjs/schedule@6.1.0(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)': + '@nestjs/schedule@6.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) cron: 4.3.5 '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.9.3)': @@ -15698,40 +16459,40 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.2.3(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.2.5(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(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.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.11(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.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.12(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 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.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-express@11.1.11)': + '@nestjs/testing@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-express@11.1.12)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) + '@nestjs/platform-express': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) - '@nestjs/websockets@11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@nestjs/platform-socket.io@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/websockets@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@nestjs/platform-socket.io@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(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.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.11)(rxjs@7.8.2) + '@nestjs/platform-socket.io': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.12)(rxjs@7.8.2) '@noble/hashes@1.8.0': {} @@ -15767,124 +16528,130 @@ snapshots: '@oazapfts/runtime@1.1.0': {} - '@opentelemetry/api-logs@0.208.0': + '@opentelemetry/api-logs@0.210.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.210.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + yaml: 2.8.2 + + '@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-grpc@0.210.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.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.210.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.210.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.210.0 + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.210.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.210.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.210.0 + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.210.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.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-http@0.210.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.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-proto@0.210.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.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-prometheus@0.210.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.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-grpc@0.210.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.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-http@0.210.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.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-proto@0.210.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.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-zipkin@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-zipkin@2.4.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-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 '@opentelemetry/host-metrics@0.36.2(@opentelemetry/api@1.9.0)': @@ -15892,158 +16659,160 @@ snapshots: '@opentelemetry/api': 1.9.0 systeminformation: 5.23.8 - '@opentelemetry/instrumentation-http@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-http@0.210.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/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.57.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-ioredis@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/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.38.2 '@opentelemetry/semantic-conventions': 1.38.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-nestjs-core@0.55.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-nestjs-core@0.56.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.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pg@0.61.2(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-pg@0.62.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/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.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.210.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.208.0 + '@opentelemetry/api-logs': 0.210.0 import-in-the-middle: 2.0.0 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.210.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.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.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.210.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.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.210.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.210.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.210.0 + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) + protobufjs: 8.0.0 - '@opentelemetry/propagator-b3@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-b3@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/propagator-jaeger@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/propagator-jaeger@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common@0.38.2': {} - '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/resources@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-logs@0.210.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.210.0 + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-metrics@2.4.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/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node@0.208.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-node@0.210.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/api-logs': 0.210.0 + '@opentelemetry/configuration': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/context-async-hooks': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.210.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 transitivePeerDependencies: - supports-color - '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-base@2.4.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/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-trace-node@2.4.0(@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.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions@1.38.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.4.0(@opentelemetry/api@1.9.0) '@paralleldrive/cuid2@2.3.1': dependencies: @@ -16110,32 +16879,32 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true - '@photo-sphere-viewer/core@5.14.0': + '@photo-sphere-viewer/core@5.14.1': dependencies: three: 0.179.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': {} @@ -16277,7 +17046,7 @@ snapshots: '@react-email/render@1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: html-to-text: 9.0.5 - prettier: 3.7.4 + prettier: 3.8.0 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) react-promise-suspense: 0.3.4 @@ -16304,78 +17073,87 @@ snapshots: '@codemirror/state': 6.5.3 '@codemirror/view': 6.39.8 - '@rollup/pluginutils@5.3.0(rollup@4.53.4)': + '@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': {} @@ -16401,7 +17179,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 @@ -16415,59 +17193,59 @@ snapshots: micromark-util-character: 1.2.0 micromark-util-symbol: 1.1.0 - '@smithy/abort-controller@4.2.6': + '@smithy/abort-controller@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/config-resolver@4.4.4': + '@smithy/config-resolver@4.4.6': dependencies: - '@smithy/node-config-provider': 4.3.6 - '@smithy/types': 4.10.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 '@smithy/util-config-provider': 4.2.0 - '@smithy/util-endpoints': 3.2.6 - '@smithy/util-middleware': 4.2.6 + '@smithy/util-endpoints': 3.2.8 + '@smithy/util-middleware': 4.2.8 tslib: 2.8.1 - '@smithy/core@3.19.0': + '@smithy/core@3.20.7': dependencies: - '@smithy/middleware-serde': 4.2.7 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 + '@smithy/middleware-serde': 4.2.9 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.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-middleware': 4.2.8 + '@smithy/util-stream': 4.5.10 '@smithy/util-utf8': 4.2.0 '@smithy/uuid': 1.1.0 tslib: 2.8.1 - '@smithy/credential-provider-imds@4.2.6': + '@smithy/credential-provider-imds@4.2.8': 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 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 tslib: 2.8.1 - '@smithy/fetch-http-handler@5.3.7': + '@smithy/fetch-http-handler@5.3.9': dependencies: - '@smithy/protocol-http': 5.3.6 - '@smithy/querystring-builder': 4.2.6 - '@smithy/types': 4.10.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 '@smithy/util-base64': 4.3.0 tslib: 2.8.1 - '@smithy/hash-node@4.2.6': + '@smithy/hash-node@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 '@smithy/util-buffer-from': 4.2.0 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@smithy/invalid-dependency@4.2.6': + '@smithy/invalid-dependency@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 '@smithy/is-array-buffer@2.2.0': @@ -16478,120 +17256,120 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/middleware-content-length@4.2.6': + '@smithy/middleware-content-length@4.2.8': dependencies: - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/middleware-endpoint@4.4.0': + '@smithy/middleware-endpoint@4.4.8': 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 + '@smithy/core': 3.20.7 + '@smithy/middleware-serde': 4.2.9 + '@smithy/node-config-provider': 4.3.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 + '@smithy/url-parser': 4.2.8 + '@smithy/util-middleware': 4.2.8 tslib: 2.8.1 - '@smithy/middleware-retry@4.4.16': + '@smithy/middleware-retry@4.4.24': 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/node-config-provider': 4.3.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/service-error-classification': 4.2.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 + '@smithy/util-middleware': 4.2.8 + '@smithy/util-retry': 4.2.8 '@smithy/uuid': 1.1.0 tslib: 2.8.1 - '@smithy/middleware-serde@4.2.7': + '@smithy/middleware-serde@4.2.9': dependencies: - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/middleware-stack@4.2.6': + '@smithy/middleware-stack@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/node-config-provider@4.3.6': + '@smithy/node-config-provider@4.3.8': dependencies: - '@smithy/property-provider': 4.2.6 - '@smithy/shared-ini-file-loader': 4.4.1 - '@smithy/types': 4.10.0 + '@smithy/property-provider': 4.2.8 + '@smithy/shared-ini-file-loader': 4.4.3 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/node-http-handler@4.4.6': + '@smithy/node-http-handler@4.4.8': dependencies: - '@smithy/abort-controller': 4.2.6 - '@smithy/protocol-http': 5.3.6 - '@smithy/querystring-builder': 4.2.6 - '@smithy/types': 4.10.0 + '@smithy/abort-controller': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/querystring-builder': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/property-provider@4.2.6': + '@smithy/property-provider@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/protocol-http@5.3.6': + '@smithy/protocol-http@5.3.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/querystring-builder@4.2.6': + '@smithy/querystring-builder@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 '@smithy/util-uri-escape': 4.2.0 tslib: 2.8.1 - '@smithy/querystring-parser@4.2.6': + '@smithy/querystring-parser@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/service-error-classification@4.2.6': + '@smithy/service-error-classification@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 - '@smithy/shared-ini-file-loader@4.4.1': + '@smithy/shared-ini-file-loader@4.4.3': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/signature-v4@5.3.6': + '@smithy/signature-v4@5.3.8': dependencies: '@smithy/is-array-buffer': 4.2.0 - '@smithy/protocol-http': 5.3.6 - '@smithy/types': 4.10.0 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 '@smithy/util-hex-encoding': 4.2.0 - '@smithy/util-middleware': 4.2.6 + '@smithy/util-middleware': 4.2.8 '@smithy/util-uri-escape': 4.2.0 '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 - '@smithy/smithy-client@4.10.1': + '@smithy/smithy-client@4.10.9': 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 + '@smithy/core': 3.20.7 + '@smithy/middleware-endpoint': 4.4.8 + '@smithy/middleware-stack': 4.2.8 + '@smithy/protocol-http': 5.3.8 + '@smithy/types': 4.12.0 + '@smithy/util-stream': 4.5.10 tslib: 2.8.1 - '@smithy/types@4.10.0': + '@smithy/types@4.12.0': dependencies: tslib: 2.8.1 - '@smithy/url-parser@4.2.6': + '@smithy/url-parser@4.2.8': dependencies: - '@smithy/querystring-parser': 4.2.6 - '@smithy/types': 4.10.0 + '@smithy/querystring-parser': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 '@smithy/util-base64@4.3.0': @@ -16622,49 +17400,49 @@ snapshots: dependencies: tslib: 2.8.1 - '@smithy/util-defaults-mode-browser@4.3.15': + '@smithy/util-defaults-mode-browser@4.3.23': dependencies: - '@smithy/property-provider': 4.2.6 - '@smithy/smithy-client': 4.10.1 - '@smithy/types': 4.10.0 + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-defaults-mode-node@4.2.18': + '@smithy/util-defaults-mode-node@4.2.26': 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 + '@smithy/config-resolver': 4.4.6 + '@smithy/credential-provider-imds': 4.2.8 + '@smithy/node-config-provider': 4.3.8 + '@smithy/property-provider': 4.2.8 + '@smithy/smithy-client': 4.10.9 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-endpoints@3.2.6': + '@smithy/util-endpoints@3.2.8': dependencies: - '@smithy/node-config-provider': 4.3.6 - '@smithy/types': 4.10.0 + '@smithy/node-config-provider': 4.3.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 '@smithy/util-hex-encoding@4.2.0': dependencies: tslib: 2.8.1 - '@smithy/util-middleware@4.2.6': + '@smithy/util-middleware@4.2.8': dependencies: - '@smithy/types': 4.10.0 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-retry@4.2.6': + '@smithy/util-retry@4.2.8': dependencies: - '@smithy/service-error-classification': 4.2.6 - '@smithy/types': 4.10.0 + '@smithy/service-error-classification': 4.2.8 + '@smithy/types': 4.12.0 tslib: 2.8.1 - '@smithy/util-stream@4.5.7': + '@smithy/util-stream@4.5.10': dependencies: - '@smithy/fetch-http-handler': 5.3.7 - '@smithy/node-http-handler': 4.4.6 - '@smithy/types': 4.10.0 + '@smithy/fetch-http-handler': 5.3.9 + '@smithy/node-http-handler': 4.4.8 + '@smithy/types': 4.12.0 '@smithy/util-base64': 4.3.0 '@smithy/util-buffer-from': 4.2.0 '@smithy/util-hex-encoding': 4.2.0 @@ -16708,33 +17486,33 @@ snapshots: dependencies: acorn: 8.15.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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/kit': 2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.53.4)(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(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.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rollup@4.55.1)(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.46.1 - svelte-parse-markup: 0.1.5(svelte@5.46.1) - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-imagetools: 9.0.2(rollup@4.53.4) + svelte: 5.48.0 + svelte-parse-markup: 0.1.5(svelte@5.48.0) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-imagetools: 9.0.2(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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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 cookie: 0.6.0 - devalue: 5.6.1 + devalue: 5.6.2 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 @@ -16742,29 +17520,30 @@ snapshots: sade: 1.8.1 set-cookie-parser: 2.7.2 sirv: 3.0.2 - svelte: 5.46.1 - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + svelte: 5.48.0 + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) debug: 4.4.3 - svelte: 5.46.1 - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + svelte: 5.48.0 + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(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.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.46.1 - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(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@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + obug: 2.1.1 + svelte: 5.48.0 + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color @@ -16983,17 +17762,17 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - '@tailwindcss/vite@4.1.18(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.28.6 + '@babel/runtime': 7.28.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 @@ -17010,18 +17789,18 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/svelte-core@1.0.0(svelte@5.46.1)': + '@testing-library/svelte-core@1.0.0(svelte@5.48.0)': dependencies: - svelte: 5.46.1 + svelte: 5.48.0 - '@testing-library/svelte@5.3.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(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.0.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@testing-library/svelte@5.3.1(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@testing-library/dom': 10.4.1 - '@testing-library/svelte-core': 1.0.0(svelte@5.46.1) - svelte: 5.46.1 + '@testing-library/svelte-core': 1.0.0(svelte@5.48.0) + svelte: 5.48.0 optionalDependencies: - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(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.0.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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: @@ -17041,28 +17820,28 @@ snapshots: '@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.10.9 '@types/archiver@7.0.0': dependencies: @@ -17074,16 +17853,16 @@ snapshots: '@types/bcrypt@6.0.0': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/bonjour@3.5.13': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/braces@3.0.5': {} @@ -17105,21 +17884,21 @@ snapshots: '@types/cli-progress@3.11.6': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/compression@1.8.1': dependencies: '@types/express': 5.0.6 - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@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.10.9 '@types/connect@3.4.38': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/content-disposition@0.5.9': {} @@ -17136,11 +17915,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.10.9 '@types/cors@2.8.19': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 + + '@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: @@ -17150,13 +18046,13 @@ snapshots: '@types/docker-modem@3.0.6': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/ssh2': 1.15.5 '@types/dockerode@3.3.47': dependencies: '@types/docker-modem': 3.0.6 - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/ssh2': 1.15.5 '@types/dom-to-image@2.6.7': {} @@ -17179,14 +18075,14 @@ snapshots: '@types/express-serve-static-core@4.19.7': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@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.10.9 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -17212,7 +18108,7 @@ snapshots: '@types/fluent-ffmpeg@2.1.28': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/geojson-vt@3.2.5': dependencies: @@ -17244,7 +18140,7 @@ snapshots: '@types/http-proxy@1.17.17': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/inquirer@8.2.12': dependencies: @@ -17268,7 +18164,7 @@ snapshots: '@types/jsonwebtoken@9.0.10': dependencies: '@types/ms': 2.1.0 - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/justified-layout@4.1.4': {} @@ -17287,7 +18183,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.10.9 '@types/leaflet@1.9.21': dependencies: @@ -17295,9 +18191,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': {} @@ -17317,7 +18213,7 @@ snapshots: '@types/mock-fs@4.13.4': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/ms@2.1.0': {} @@ -17327,7 +18223,7 @@ snapshots: '@types/node-forge@1.3.14': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/node@17.0.45': {} @@ -17335,23 +18231,23 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.19.27': + '@types/node@20.19.30': dependencies: undici-types: 6.21.0 - '@types/node@24.10.4': + '@types/node@24.10.9': dependencies: undici-types: 7.16.0 - '@types/node@25.0.3': + '@types/node@25.0.9': dependencies: undici-types: 7.16.0 optional: true - '@types/nodemailer@7.0.4': + '@types/nodemailer@7.0.5': dependencies: - '@aws-sdk/client-sesv2': 3.952.0 - '@types/node': 24.10.4 + '@aws-sdk/client-sesv2': 3.971.0 + '@types/node': 24.10.9 transitivePeerDependencies: - aws-crt @@ -17359,37 +18255,37 @@ snapshots: dependencies: '@types/keygrip': 1.0.6 '@types/koa': 3.0.1 - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@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.10.9 + 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.10.9 + 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.10.9 '@types/prismjs@1.26.5': {} '@types/qrcode@1.5.6': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/qs@6.14.0': {} @@ -17398,27 +18294,27 @@ snapshots: '@types/react-router-config@5.0.11': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.7 + '@types/react': 19.2.8 '@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.8 '@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.8 - '@types/react@19.2.7': + '@types/react@19.2.8': dependencies: csstype: 3.2.3 '@types/readdir-glob@1.1.5': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/retry@0.12.2': {} @@ -17428,18 +18324,18 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/semver@7.7.1': {} '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/send@1.2.1': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/serve-index@1.9.4': dependencies: @@ -17448,25 +18344,25 @@ snapshots: '@types/serve-static@1.15.10': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@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.10.9 '@types/sockjs@0.3.36': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/ssh2-streams@0.1.13': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/ssh2@0.5.52': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/ssh2-streams': 0.1.13 '@types/ssh2@1.15.5': @@ -17477,7 +18373,7 @@ snapshots: dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 24.10.4 + '@types/node': 24.10.9 form-data: 4.0.5 '@types/supercluster@7.1.3': @@ -17491,7 +18387,10 @@ snapshots: '@types/through@0.0.33': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 + + '@types/trusted-types@2.0.7': + optional: true '@types/ua-parser-js@0.7.39': {} @@ -17505,7 +18404,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 '@types/yargs-parser@21.0.3': {} @@ -17513,14 +18412,14 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.51.0(@typescript-eslint/parser@8.51.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.53.0(@typescript-eslint/parser@8.53.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)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.51.0 - '@typescript-eslint/type-utils': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.51.0 + '@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.53.0 + '@typescript-eslint/type-utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.0 eslint: 9.39.2(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 @@ -17529,41 +18428,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.51.0 - '@typescript-eslint/types': 8.51.0 - '@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.51.0 + '@typescript-eslint/scope-manager': 8.53.0 + '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.53.0 debug: 4.4.3 eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.51.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.53.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.51.0(typescript@5.9.3) - '@typescript-eslint/types': 8.51.0 + '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) + '@typescript-eslint/types': 8.53.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.51.0': + '@typescript-eslint/scope-manager@8.53.0': dependencies: - '@typescript-eslint/types': 8.51.0 - '@typescript-eslint/visitor-keys': 8.51.0 + '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/visitor-keys': 8.53.0 - '@typescript-eslint/tsconfig-utils@8.51.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.53.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.51.0 - '@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.0(eslint@9.39.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.4.0(typescript@5.9.3) @@ -17571,14 +18470,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.51.0': {} + '@typescript-eslint/types@8.53.0': {} - '@typescript-eslint/typescript-estree@8.51.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.53.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.51.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.51.0(typescript@5.9.3) - '@typescript-eslint/types': 8.51.0 - '@typescript-eslint/visitor-keys': 8.51.0 + '@typescript-eslint/project-service': 8.53.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.53.0(typescript@5.9.3) + '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/visitor-keys': 8.53.0 debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 @@ -17588,27 +18487,27 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.51.0 - '@typescript-eslint/types': 8.51.0 - '@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.53.0 + '@typescript-eslint/types': 8.53.0 + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.51.0': + '@typescript-eslint/visitor-keys@8.53.0': dependencies: - '@typescript-eslint/types': 8.51.0 + '@typescript-eslint/types': 8.53.0 eslint-visitor-keys: 4.2.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@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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 @@ -17623,11 +18522,11 @@ 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@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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.0.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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 @@ -17642,7 +18541,7 @@ 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@25.0.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -17654,21 +18553,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)(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@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(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)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@3.2.4(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(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.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -17780,10 +18679,10 @@ snapshots: dependencies: '@namnode/store': 0.1.0 - '@zoom-image/svelte@0.3.8(svelte@5.46.1)': + '@zoom-image/svelte@0.3.8(svelte@5.48.0)': dependencies: '@zoom-image/core': 0.41.4 - svelte: 5.46.1 + svelte: 5.48.0 abab@2.0.6: optional: true @@ -17949,7 +18848,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 @@ -18038,12 +18937,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: @@ -18144,16 +19043,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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1): + bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0): 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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1) - svelte: 5.46.1 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1) - tabbable: 6.3.0 + runed: 0.35.1(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0) + svelte: 5.48.0 + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0) + tabbable: 6.4.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -18173,22 +19072,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: @@ -18267,10 +19166,10 @@ snapshots: builtin-modules@5.0.0: {} - bullmq@5.66.4: + bullmq@5.66.5: dependencies: cron-parser: 4.9.0 - ioredis: 5.8.2 + ioredis: 5.9.1 msgpackr: 1.11.5 node-abort-controller: 3.1.1 semver: 7.7.3 @@ -18436,6 +19335,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 @@ -18637,6 +19550,8 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 + confbox@0.1.8: {} + confbox@0.2.2: {} config-chain@1.1.13: @@ -18694,7 +19609,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 @@ -18702,7 +19617,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: @@ -18719,6 +19634,14 @@ snapshots: 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 @@ -18778,7 +19701,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 @@ -18789,9 +19712,9 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.3 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) @@ -18799,7 +19722,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 @@ -18923,19 +19846,195 @@ snapshots: 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 + dagre-d3-es@7.0.13: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.23 + data-urls@3.0.2: dependencies: abab: 2.0.6 @@ -18949,6 +20048,8 @@ snapshots: whatwg-url: 14.2.0 optional: true + dayjs@1.11.19: {} + debounce@1.2.1: {} debounce@2.2.0: {} @@ -19021,6 +20122,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: {} @@ -19051,7 +20156,7 @@ snapshots: transitivePeerDependencies: - supports-color - devalue@5.6.1: {} + devalue@5.6.2: {} devlop@1.1.0: dependencies: @@ -19109,9 +20214,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.8)(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.8)(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 @@ -19166,6 +20271,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 @@ -19251,7 +20360,7 @@ snapshots: engine.io@6.6.5: dependencies: '@types/cors': 2.8.19 - '@types/node': 24.10.4 + '@types/node': 24.10.9 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -19289,6 +20398,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 @@ -19460,17 +20571,17 @@ snapshots: lodash.memoize: 4.1.2 semver: 7.7.3 - 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@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0): dependencies: eslint: 9.39.2(jiti@2.6.1) - prettier: 3.7.4 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.11 + prettier: 3.8.0 + 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-plugin-svelte@3.13.1(eslint@9.39.2(jiti@2.6.1))(svelte@5.46.1): + eslint-plugin-svelte@3.14.0(eslint@9.39.2(jiti@2.6.1))(svelte@5.48.0): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) '@jridgewell/sourcemap-codec': 1.5.5 @@ -19482,9 +20593,9 @@ snapshots: 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.46.1) + svelte-eslint-parser: 1.4.1(svelte@5.48.0) optionalDependencies: - svelte: 5.46.1 + svelte: 5.48.0 transitivePeerDependencies: - ts-node @@ -19647,7 +20758,7 @@ snapshots: eval@0.1.8: dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 require-like: 0.1.2 event-emitter@0.3.5: @@ -19724,7 +20835,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 @@ -19740,7 +20851,7 @@ snapshots: 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 @@ -19759,7 +20870,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 @@ -19855,17 +20966,17 @@ 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.2.0: + file-type@21.3.0: dependencies: '@tokenizer/inflate': 0.4.1 strtok3: 10.3.4 @@ -19944,9 +21055,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.8(@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.8(@swc/helpers@0.5.17))): dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.28.6 chalk: 4.1.2 chokidar: 4.0.3 cosmiconfig: 8.3.6(typescript@5.9.3) @@ -19959,7 +21070,7 @@ snapshots: semver: 7.7.3 tapable: 2.3.0 typescript: 5.9.3 - webpack: 5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.17)) + webpack: 5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17)) form-data-encoder@2.1.4: {} @@ -20039,10 +21150,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 @@ -20205,6 +21316,8 @@ snapshots: dependencies: duplexer: 0.1.2 + hachure-fill@0.5.2: {} + handle-thing@2.0.1: {} handlebars@4.7.8: @@ -20216,11 +21329,16 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - happy-dom@20.0.11: + happy-dom@20.3.0: dependencies: - '@types/node': 20.19.27 + '@types/node': 20.19.30 '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 whatwg-mimetype: 3.0.0 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate has-flag@4.0.0: {} @@ -20392,7 +21510,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 @@ -20459,15 +21577,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: @@ -20587,9 +21705,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 @@ -20650,15 +21767,15 @@ snapshots: inline-style-parser@0.2.7: {} - inquirer@8.2.7(@types/node@24.10.4): + inquirer@8.2.7(@types/node@24.10.9): dependencies: - '@inquirer/external-editor': 1.0.3(@types/node@24.10.4) + '@inquirer/external-editor': 1.0.3(@types/node@24.10.9) 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 @@ -20670,6 +21787,8 @@ snapshots: transitivePeerDependencies: - '@types/node' + internmap@1.0.1: {} + internmap@2.0.3: {} intl-messageformat@10.7.18: @@ -20679,20 +21798,20 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.11.4 tslib: 2.8.1 - intl-messageformat@11.0.8: + intl-messageformat@11.0.9: dependencies: - '@formatjs/ecma402-abstract': 3.0.7 - '@formatjs/fast-memoize': 3.0.2 - '@formatjs/icu-messageformat-parser': 3.2.1 + '@formatjs/ecma402-abstract': 3.0.8 + '@formatjs/fast-memoize': 3.0.3 + '@formatjs/icu-messageformat-parser': 3.3.0 tslib: 2.8.1 invariant@2.2.4: dependencies: loose-envify: 1.4.0 - ioredis@5.8.2: + ioredis@5.9.1: 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 @@ -20873,7 +21992,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.10.4 + '@types/node': 24.10.9 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -20881,13 +22000,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 24.10.4 + '@types/node': 24.10.9 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -20951,7 +22070,7 @@ snapshots: whatwg-encoding: 2.0.0 whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 - ws: 8.18.3 + ws: 8.19.0 xml-name-validator: 4.0.0 optionalDependencies: canvas: 2.11.2 @@ -20981,7 +22100,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.18.3 + ws: 8.19.0 xml-name-validator: 5.0.0 optionalDependencies: canvas: 2.11.2(encoding@0.1.13) @@ -21011,7 +22130,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.18.3 + ws: 8.19.0 xml-name-validator: 5.0.0 optionalDependencies: canvas: 2.11.2 @@ -21087,6 +22206,10 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 + katex@0.16.27: + dependencies: + commander: 8.3.0 + kdbush@3.0.0: {} kdbush@4.0.2: {} @@ -21099,6 +22222,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + khroma@2.1.0: {} + kind-of@6.0.3: {} kleur@3.0.3: {} @@ -21130,14 +22255,22 @@ 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.2)(postgres@3.4.8): dependencies: kysely: 0.28.2 optionalDependencies: - postgres: 3.4.7 + postgres: 3.4.8 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 @@ -21147,6 +22280,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 @@ -21243,7 +22380,9 @@ snapshots: dependencies: p-locate: 6.0.0 - lodash-es@4.17.22: {} + lodash-es@4.17.21: {} + + lodash-es@4.17.23: {} lodash.camelcase@4.3.0: {} @@ -21275,6 +22414,8 @@ snapshots: lodash@4.17.21: {} + lodash@4.17.23: {} + log-symbols@4.1.0: dependencies: chalk: 4.1.2 @@ -21389,7 +22530,7 @@ snapshots: tinyqueue: 2.0.3 vt-pbf: 3.1.3 - maplibre-gl@5.15.0: + maplibre-gl@5.16.0: dependencies: '@mapbox/geojson-rewind': 0.5.2 '@mapbox/jsonlint-lines-primitives': 2.0.2 @@ -21661,6 +22802,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: @@ -21998,11 +23162,11 @@ 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: {} @@ -22075,6 +23239,13 @@ snapshots: mkdirp@1.0.4: {} + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.2 + mnemonist@0.40.3: dependencies: obliterator: 2.0.5 @@ -22130,6 +23301,8 @@ snapshots: mute-stream@2.0.0: {} + mute-stream@3.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -22162,39 +23335,39 @@ snapshots: neo-async@2.6.2: {} - nest-commander@3.20.1(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(@types/inquirer@8.2.12)(@types/node@24.10.4)(typescript@5.9.3): + nest-commander@3.20.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(@types/inquirer@8.2.12)(@types/node@24.10.9)(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.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11) - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(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.10.9) transitivePeerDependencies: - '@types/node' - typescript - nestjs-cls@5.4.3(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2): + nestjs-cls@5.4.3(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(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.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11)(kysely@0.28.2)(reflect-metadata@0.2.2): + nestjs-kysely@3.1.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(kysely@0.28.2)(reflect-metadata@0.2.2): dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(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.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.11): + nestjs-otel@7.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12): dependencies: - '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.11(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.11)(@nestjs/websockets@11.1.11)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(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 @@ -22218,7 +23391,7 @@ snapshots: node-emoji@1.11.0: dependencies: - lodash: 4.17.21 + lodash: 4.17.23 node-emoji@2.2.0: dependencies: @@ -22303,11 +23476,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 @@ -22343,6 +23516,8 @@ snapshots: obuf@1.1.2: {} + obug@2.1.1: {} + oidc-provider@9.6.0: dependencies: '@koa/cors': 5.0.0 @@ -22489,6 +23664,8 @@ snapshots: registry-url: 6.0.1 semver: 7.7.3 + package-manager-detector@1.6.0: {} + param-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -22510,7 +23687,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.28.6 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -22542,6 +23719,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: {} @@ -22596,36 +23775,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.10.0: {} pg-int8@1.0.1: {} - pg-pool@3.10.1(pg@8.16.3): + pg-pool@3.11.0(pg@8.17.1): dependencies: - pg: 8.16.3 + pg: 8.17.1 - 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.17.1: 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.10.0 + pg-pool: 3.11.0(pg@8.17.1) + 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: @@ -22647,6 +23826,12 @@ 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 @@ -22680,6 +23865,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 @@ -22850,13 +24042,13 @@ snapshots: 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 + webpack: 5.104.1 transitivePeerDependencies: - typescript @@ -23162,7 +24354,7 @@ snapshots: postgres-array@2.0.0: {} - postgres-bytea@1.0.0: {} + postgres-bytea@1.0.1: {} postgres-date@1.0.7: {} @@ -23170,7 +24362,7 @@ snapshots: dependencies: xtend: 4.0.2 - postgres@3.4.7: {} + postgres@3.4.8: {} potpack@1.0.2: {} @@ -23178,29 +24370,29 @@ snapshots: 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.0)(typescript@5.9.3): dependencies: - prettier: 3.7.4 + prettier: 3.8.0 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.0): dependencies: - prettier: 3.7.4 + prettier: 3.8.0 - prettier-plugin-svelte@3.4.1(prettier@3.7.4)(svelte@5.46.1): + prettier-plugin-svelte@3.4.1(prettier@3.8.0)(svelte@5.48.0): dependencies: - prettier: 3.7.4 - svelte: 5.46.1 + prettier: 3.8.0 + svelte: 5.48.0 - prettier@3.7.4: {} + prettier@3.8.0: {} pretty-error@4.0.0: dependencies: - lodash: 4.17.21 + lodash: 4.17.23 renderkid: 3.0.0 pretty-format@27.5.1: @@ -23271,7 +24463,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.10.9 + 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.10.9 long: 5.3.2 protocol-buffers-schema@3.6.0: {} @@ -23305,7 +24512,7 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 - qs@6.14.0: + qs@6.14.1: dependencies: side-channel: 1.1.0 @@ -23348,14 +24555,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: @@ -23408,11 +24615,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: @@ -23420,13 +24627,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 @@ -23437,7 +24644,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 @@ -23658,7 +24865,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: {} @@ -23731,43 +24938,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 @@ -23796,14 +25013,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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1): + runed@0.35.1(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.46.1 + svelte: 5.48.0 optionalDependencies: - '@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@sveltejs/kit': 2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) rw@1.3.3: {} @@ -24245,7 +25462,7 @@ snapshots: sprintf-js@1.0.3: {} - sql-formatter@15.6.12: + sql-formatter@15.7.0: dependencies: argparse: 2.0.1 nearley: 2.20.1 @@ -24381,6 +25598,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 @@ -24391,7 +25610,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 @@ -24401,7 +25620,7 @@ snapshots: formidable: 3.5.4 methods: 1.1.2 mime: 2.6.0 - qs: 6.14.0 + qs: 6.14.1 transitivePeerDependencies: - supports-color @@ -24413,10 +25632,11 @@ snapshots: 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 @@ -24430,23 +25650,23 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-awesome@3.3.5(svelte@5.46.1): + svelte-awesome@3.3.5(svelte@5.48.0): dependencies: - svelte: 5.46.1 + svelte: 5.48.0 - svelte-check@4.3.5(picomatch@4.0.3)(svelte@5.46.1)(typescript@5.9.3): + svelte-check@4.3.5(picomatch@4.0.3)(svelte@5.48.0)(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.46.1 + svelte: 5.48.0 typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.4.1(svelte@5.46.1): + svelte-eslint-parser@1.4.1(svelte@5.48.0): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -24455,7 +25675,7 @@ snapshots: postcss-scss: 4.0.9(postcss@8.5.6) postcss-selector-parser: 7.1.1 optionalDependencies: - svelte: 5.46.1 + svelte: 5.48.0 svelte-floating-ui@1.5.8: dependencies: @@ -24468,7 +25688,7 @@ snapshots: dependencies: highlight.js: 11.11.1 - svelte-i18n@4.0.1(svelte@5.46.1): + svelte-i18n@4.0.1(svelte@5.48.0): dependencies: cli-color: 2.0.4 deepmerge: 4.3.1 @@ -24476,10 +25696,10 @@ snapshots: estree-walker: 2.0.2 intl-messageformat: 10.7.18 sade: 1.8.1 - svelte: 5.46.1 + svelte: 5.48.0 tiny-glob: 0.2.9 - svelte-jsoneditor@3.11.0(svelte@5.46.1): + svelte-jsoneditor@3.11.0(svelte@5.48.0): dependencies: '@codemirror/autocomplete': 6.20.0 '@codemirror/commands': 6.10.1 @@ -24502,46 +25722,46 @@ snapshots: json-source-map: 0.6.1 jsonpath-plus: 10.3.0 jsonrepair: 3.13.1 - lodash-es: 4.17.22 + lodash-es: 4.17.23 memoize-one: 6.0.0 natural-compare-lite: 1.4.0 sass: 1.97.1 - svelte: 5.46.1 - svelte-awesome: 3.3.5(svelte@5.46.1) + svelte: 5.48.0 + svelte-awesome: 3.3.5(svelte@5.48.0) svelte-select: 5.8.3 vanilla-picker: 2.12.3 - svelte-maplibre@1.2.5(svelte@5.46.1): + svelte-maplibre@1.2.5(svelte@5.48.0): dependencies: d3-geo: 3.1.1 dequal: 2.0.3 just-compare: 2.3.0 - maplibre-gl: 5.15.0 + maplibre-gl: 5.16.0 pmtiles: 3.2.1 - svelte: 5.46.1 + svelte: 5.48.0 - svelte-parse-markup@0.1.5(svelte@5.46.1): + svelte-parse-markup@0.1.5(svelte@5.48.0): dependencies: - svelte: 5.46.1 + svelte: 5.48.0 - svelte-persisted-store@0.12.0(svelte@5.46.1): + svelte-persisted-store@0.12.0(svelte@5.48.0): dependencies: - svelte: 5.46.1 + svelte: 5.48.0 svelte-select@5.8.3: dependencies: svelte-floating-ui: 1.5.8 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0): 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.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1) + runed: 0.35.1(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.48.0) style-to-object: 1.0.14 - svelte: 5.46.1 + svelte: 5.48.0 transitivePeerDependencies: - '@sveltejs/kit' - svelte@5.46.1: + svelte@5.48.0: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -24551,7 +25771,7 @@ snapshots: aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.6.1 + devalue: 5.6.2 esm-env: 1.2.2 esrap: 2.2.1 is-reference: 3.0.3 @@ -24571,7 +25791,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 @@ -24586,13 +25806,13 @@ 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: {} @@ -24701,25 +25921,25 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - terser-webpack-plugin@5.3.16(@swc/core@1.15.8(@swc/helpers@0.5.17))(webpack@5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.17))): + terser-webpack-plugin@5.3.16(@swc/core@1.15.8(@swc/helpers@0.5.17))(webpack@5.104.1(@swc/core@1.15.8(@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(@swc/core@1.15.8(@swc/helpers@0.5.17)) + webpack: 5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17)) optionalDependencies: '@swc/core': 1.15.8(@swc/helpers@0.5.17) - terser-webpack-plugin@5.3.16(webpack@5.103.0): + 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.103.0 + webpack: 5.104.1 terser@5.44.1: dependencies: @@ -24807,6 +26027,8 @@ snapshots: tinyexec@0.3.2: {} + tinyexec@1.0.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -24876,6 +26098,8 @@ snapshots: 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 @@ -24894,6 +26118,8 @@ snapshots: dependencies: typescript: 5.9.3 + ts-dedent@2.2.0: {} + ts-interface-checker@0.1.13: {} tsconfck@3.1.6(typescript@5.9.3): @@ -24955,12 +26181,12 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.51.0(@typescript-eslint/parser@8.51.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.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.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.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.53.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.53.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 transitivePeerDependencies: @@ -24970,12 +26196,14 @@ snapshots: ua-is-frozen@0.1.2: {} - ua-parser-js@2.0.7: + ua-parser-js@2.0.8: 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 @@ -25095,9 +26323,9 @@ snapshots: unpipe@1.0.0: {} - unplugin-swc@1.5.9(@swc/core@1.15.8(@swc/helpers@0.5.17))(rollup@4.53.4): + unplugin-swc@1.5.9(@swc/core@1.15.8(@swc/helpers@0.5.17))(rollup@4.55.1): dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.53.4) + '@rollup/pluginutils': 5.3.0(rollup@4.55.1) '@swc/core': 1.15.8(@swc/helpers@0.5.17) load-tsconfig: 0.2.5 unplugin: 2.3.11 @@ -25140,14 +26368,14 @@ 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) + file-loader: 6.2.0(webpack@5.104.1) url-parse@1.5.10: dependencies: @@ -25158,7 +26386,7 @@ snapshots: url@0.11.4: dependencies: punycode: 1.4.1 - qs: 6.14.0 + qs: 6.14.1 urlpattern-polyfill@8.0.2: {} @@ -25229,22 +26457,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.2(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)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite-node@3.2.4(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(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)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -25259,13 +26487,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite-node@3.2.4(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -25280,27 +26508,27 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@6.0.3(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + vite-tsconfig-paths@6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(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)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(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)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.2 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.10.9 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 @@ -25309,16 +26537,16 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite@7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.2 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': 25.0.3 + '@types/node': 25.0.9 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 @@ -25327,19 +26555,19 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitefu@1.1.1(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(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.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): optionalDependencies: - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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.10.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(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.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(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)(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@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(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 @@ -25357,13 +26585,13 @@ 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)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(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 + '@types/node': 24.10.9 + happy-dom: 20.3.0 jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: - jiti @@ -25379,11 +26607,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@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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)(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@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(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 @@ -25401,13 +26629,13 @@ 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)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(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 + '@types/node': 24.10.9 + happy-dom: 20.3.0 jsdom: 26.1.0(canvas@2.11.2) transitivePeerDependencies: - jiti @@ -25423,11 +26651,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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.0.9)(happy-dom@20.3.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(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@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(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.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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 @@ -25445,13 +26673,13 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@25.0.9)(jiti@2.6.1)(lightningcss@1.30.2)(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': 25.0.3 - happy-dom: 20.0.11 + '@types/node': 25.0.9 + happy-dom: 20.3.0 jsdom: 26.1.0(canvas@2.11.2) transitivePeerDependencies: - jiti @@ -25467,6 +26695,23 @@ snapshots: - tsx - yaml + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + 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: {} + vt-pbf@3.1.3: dependencies: '@mapbox/point-geometry': 0.1.0 @@ -25485,7 +26730,7 @@ snapshots: 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 @@ -25525,7 +26770,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 @@ -25534,9 +26779,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 @@ -25564,10 +26809,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 @@ -25592,7 +26837,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 @@ -25605,7 +26850,7 @@ snapshots: browserslist: 4.28.1 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.4 - es-module-lexer: 1.7.0 + es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -25616,15 +26861,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.8(@swc/helpers@0.5.17)): + webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.17)): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -25637,7 +26882,7 @@ snapshots: browserslist: 4.28.1 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.4 - es-module-lexer: 1.7.0 + es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -25648,15 +26893,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.8(@swc/helpers@0.5.17))(webpack@5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.17))) - watchpack: 2.4.4 + terser-webpack-plugin: 5.3.16(@swc/core@1.15.8(@swc/helpers@0.5.17))(webpack@5.104.1(@swc/core@1.15.8(@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 @@ -25665,7 +26910,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: @@ -25759,6 +27004,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.2 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + wrappy@1.0.2: {} write-file-atomic@3.0.3: @@ -25772,6 +27023,8 @@ snapshots: ws@8.18.3: {} + ws@8.19.0: {} + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f7f22e6f44..be30451965 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -11,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 248216ad5b..3fe3b1570a 100644 --- a/server/.nvmrc +++ b/server/.nvmrc @@ -1 +1 @@ -24.12.0 +24.13.0 diff --git a/server/Dockerfile b/server/Dockerfile index 566eb4c913..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 \ @@ -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..be752dd862 100644 --- a/server/Dockerfile.dev +++ b/server/Dockerfile.dev @@ -1,5 +1,5 @@ # 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 \ diff --git a/server/package.json b/server/package.json index 81f1181e66..c9e2c2ac22 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "2.4.1", + "version": "2.5.2", "description": "", "author": "", "private": true, @@ -45,14 +45,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.57.0", - "@opentelemetry/instrumentation-nestjs-core": "^0.55.0", - "@opentelemetry/instrumentation-pg": "^0.61.0", + "@opentelemetry/exporter-prometheus": "^0.210.0", + "@opentelemetry/instrumentation-http": "^0.210.0", + "@opentelemetry/instrumentation-ioredis": "^0.58.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.56.0", + "@opentelemetry/instrumentation-pg": "^0.62.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", - "@opentelemetry/sdk-node": "^0.208.0", + "@opentelemetry/sdk-node": "^0.210.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@react-email/components": "^0.5.0", "@react-email/render": "^1.1.2", @@ -96,7 +96,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,6 +110,7 @@ "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" @@ -128,13 +129,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.4", + "@types/node": "^24.10.9", "@types/nodemailer": "^7.0.0", "@types/picomatch": "^4.0.0", "@types/pngjs": "^6.0.5", @@ -166,7 +167,7 @@ "vitest": "^3.0.0" }, "volta": { - "node": "24.12.0" + "node": "24.13.0" }, "overrides": { "sharp": "^0.34.5" diff --git a/server/src/app.module.ts b/server/src/app.module.ts index caa4ea4b6e..7d622ea23d 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,8 +22,11 @@ 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'; @@ -103,8 +107,12 @@ export class ApiModule extends BaseModule {} providers: [ ConfigRepository, LoggingRepository, + StorageRepository, + ProcessRepository, + DatabaseRepository, SystemMetadataRepository, AppRepository, + MaintenanceHealthRepository, MaintenanceWebsocketRepository, MaintenanceWorkerService, ...commonMiddleware, @@ -116,9 +124,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/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 96233429ff..809c7e45a8 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -141,6 +141,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 d52a40d7dd..ec6083cfa8 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -33,6 +33,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'; @@ -104,10 +105,11 @@ 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') @@ -145,7 +147,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( @@ -200,7 +203,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 56c9d18049..cf8b80be38 100644 --- a/server/src/controllers/asset.controller.spec.ts +++ b/server/src/controllers/asset.controller.spec.ts @@ -292,6 +292,64 @@ describe(AssetController.name, () => { }); }); + 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 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', () => { it('should be an authenticated route', async () => { await request(ctx.getHttpServer()).delete(`/assets/${factory.uuid()}/metadata/mobile-app`); diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index ba9ec865f9..8eb3a5ce44 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -20,6 +20,7 @@ import { UpdateAssetDto, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { AssetEditActionListDto, AssetEditsDto } 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'; @@ -65,7 +66,7 @@ export class AssetController { } @Post('jobs') - @Authenticated() + @Authenticated({ permission: Permission.JobCreate }) @HttpCode(HttpStatus.NO_CONTENT) @Endpoint({ summary: 'Run an asset job', @@ -226,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: AssetEditActionListDto, + ): 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/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/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/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/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 96623092f1..c6821404dc 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -1,7 +1,15 @@ 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, +} from 'src/enum'; import { AssetRepository } from 'src/repositories/asset.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; @@ -24,11 +32,10 @@ export interface MoveRequest { }; } -export type GeneratedImageType = AssetPathType.Preview | AssetPathType.Thumbnail | AssetPathType.FullSize; -export type GeneratedAssetType = GeneratedImageType | AssetPathType.EncodedVideo; - export type ThumbnailPathEntity = { id: string; ownerId: string }; +export type ImagePathOptions = { fileType: AssetFileType; format: ImageFormat | RawExtractedFormat; isEdited: boolean }; + let instance: StorageCore | null; let mediaLocation: string | undefined; @@ -105,8 +112,12 @@ export class StorageCore { return StorageCore.getNestedPath(StorageFolder.Thumbnails, person.ownerId, `${person.id}.jpeg`); } - static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: 'jpeg' | 'webp') { - return StorageCore.getNestedPath(StorageFolder.Thumbnails, asset.ownerId, `${asset.id}-${type}.${format}`); + 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) { @@ -131,14 +142,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 }), }); } @@ -292,19 +303,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..dd979fdea6 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,17 @@ 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', ], - 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 +401,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'], @@ -451,6 +465,7 @@ export const columns = { 'asset_exif.projectionType', 'asset_exif.rating', 'asset_exif.state', + 'asset_exif.tags', 'asset_exif.timeZone', ], plugin: [ @@ -474,4 +489,5 @@ export const lockableProperties = [ 'longitude', 'rating', 'timeZone', + 'tags', ] as const; diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 054bbf8fec..87a3900a7f 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -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.dto.ts b/server/src/dtos/album.dto.ts index 2f3f22099a..0f46ebaa42 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 = only own, undefined = all', + }) 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 262e2f9637..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); @@ -80,24 +89,26 @@ export class AssetMediaCreateDto extends AssetMediaBase { @IsArray() 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.ts b/server/src/dtos/asset-response.dto.ts index e228cd8f9f..e163b386be 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,20 @@ 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' }) thumbhash!: string | null; + @ApiPropertyOptional({ description: 'Original MIME type' }) originalMimeType?: string; @ApiProperty({ type: 'string', @@ -31,9 +37,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 +57,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 +100,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 +148,7 @@ export type MapAsset = { deviceId: string; duplicateId: string | null; duration: string | null; + edits?: AssetEditActionItem[]; encodedVideoPath: string | null; exifInfo?: Selectable | null; faces?: AssetFace[]; @@ -129,14 +171,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; } @@ -147,7 +194,11 @@ export type AssetMapOptions = { }; // TODO: this is inefficient -const peopleWithFaces = (faces?: AssetFace[]): PersonWithFacesResponseDto[] => { +const peopleWithFaces = ( + faces?: AssetFace[], + edits?: AssetEditActionItem[], + assetDimensions?: ImageDimensions, +): PersonWithFacesResponseDto[] => { const result: PersonWithFacesResponseDto[] = []; if (faces) { for (const face of faces) { @@ -156,7 +207,7 @@ const peopleWithFaces = (faces?: AssetFace[]): PersonWithFacesResponseDto[] => { if (existingPersonEntry) { existingPersonEntry.faces.push(face); } else { - result.push({ ...mapPerson(face.person!), faces: [mapFacesWithoutPerson(face)] }); + result.push({ ...mapPerson(face.person!), faces: [mapFacesWithoutPerson(face, edits, assetDimensions)] }); } } } @@ -190,10 +241,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,7 +274,7 @@ 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), + people: peopleWithFaces(entity.faces, entity.edits, assetDimensions), unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)), checksum: hexOrBufferToBase64(entity.checksum)!, stack: withStack ? mapStack(entity) : undefined, @@ -227,5 +282,8 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset 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 854c244ba9..47226e1503 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -22,6 +22,7 @@ import { AssetStats } from 'src/repositories/asset.repository'; import { IsNotSiblingOf, Optional, ValidateBoolean, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; export class DeviceIdDto { + @ApiProperty({ description: 'Device ID' }) @IsNotEmpty() @IsString() deviceId!: string; @@ -32,49 +33,57 @@ 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; + @ApiProperty({ description: 'Rating' }) @Optional() @IsInt() @Max(5) @Min(-1) rating?: number; + @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[]; + @ApiProperty({ description: 'Duplicate asset ID' }) @Optional() 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 +91,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 +105,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 +122,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; - @ValidateString() + @ValidateString({ description: 'Metadata key' }) key!: string; } export class AssetMetadataUpsertDto { + @ApiProperty({ description: 'Metadata items to upsert' }) @IsArray() @ValidateNested({ each: true }) @Type(() => AssetMetadataUpsertItemDto) @@ -154,14 +165,16 @@ export class AssetMetadataUpsertDto { } export class AssetMetadataUpsertItemDto { - @ValidateString() + @ValidateString({ description: 'Metadata key' }) key!: string; + @ApiProperty({ description: 'Metadata value (object)' }) @IsObject() value!: object; } export class AssetMetadataBulkUpsertDto { + @ApiProperty({ description: 'Metadata items to upsert' }) @IsArray() @ValidateNested({ each: true }) @Type(() => AssetMetadataBulkUpsertItemDto) @@ -169,17 +182,19 @@ export class AssetMetadataBulkUpsertDto { } export class AssetMetadataBulkUpsertItemDto { - @ValidateUUID() + @ValidateUUID({ description: 'Asset ID' }) assetId!: string; - @ValidateString() + @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) @@ -187,47 +202,57 @@ export class AssetMetadataBulkDeleteDto { } export class AssetMetadataBulkDeleteItemDto { - @ValidateUUID() + @ValidateUUID({ description: 'Asset ID' }) assetId!: string; - @ValidateString() + @ValidateString({ description: 'Metadata key' }) key!: string; } export class AssetMetadataResponseDto { - @ValidateString() + @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/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..2f877e3c0b 100644 --- a/server/src/dtos/download.dto.ts +++ b/server/src/dtos/download.dto.ts @@ -1,32 +1,34 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsInt, IsPositive } from 'class-validator'; import { Optional, 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[]; } 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..8bb1eef47b --- /dev/null +++ b/server/src/dtos/editing.dto.ts @@ -0,0 +1,130 @@ +import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { ClassConstructor, plainToInstance, Transform, Type } from 'class-transformer'; +import { ArrayMinSize, IsEnum, IsInt, Min, ValidateNested } from 'class-validator'; +import { IsAxisAlignedRotation, IsUniqueEditActions, 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; +} + +class AssetEditActionBase { + @IsEnum(AssetEditAction) + @ApiProperty({ enum: AssetEditAction, enumName: 'AssetEditAction', description: 'Type of edit action to perform' }) + action!: AssetEditAction; +} + +export class AssetEditActionCrop extends AssetEditActionBase { + @ValidateNested() + @Type(() => CropParameters) + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) + parameters!: CropParameters; +} + +export class AssetEditActionRotate extends AssetEditActionBase { + @ValidateNested() + @Type(() => RotateParameters) + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) + parameters!: RotateParameters; +} + +export class AssetEditActionMirror extends AssetEditActionBase { + @ValidateNested() + @Type(() => MirrorParameters) + // Description lives on schema to avoid duplication + @ApiProperty({ description: undefined }) + parameters!: MirrorParameters; +} + +export type AssetEditActionItem = + | { + action: AssetEditAction.Crop; + parameters: CropParameters; + } + | { + action: AssetEditAction.Rotate; + parameters: RotateParameters; + } + | { + action: AssetEditAction.Mirror; + parameters: MirrorParameters; + }; + +export type AssetEditActionParameter = { + [AssetEditAction.Crop]: CropParameters; + [AssetEditAction.Rotate]: RotateParameters; + [AssetEditAction.Mirror]: MirrorParameters; +}; + +type AssetEditActions = AssetEditActionCrop | AssetEditActionRotate | AssetEditActionMirror; +const actionToClass: Record> = { + [AssetEditAction.Crop]: AssetEditActionCrop, + [AssetEditAction.Rotate]: AssetEditActionRotate, + [AssetEditAction.Mirror]: AssetEditActionMirror, +} as const; + +const getActionClass = (item: { action: AssetEditAction }): ClassConstructor => + actionToClass[item.action]; + +@ApiExtraModels(AssetEditActionRotate, AssetEditActionMirror, AssetEditActionCrop) +export class AssetEditActionListDto { + /** list of edits */ + @ArrayMinSize(1) + @IsUniqueEditActions() + @ValidateNested({ each: true }) + @Transform(({ value: edits }) => + Array.isArray(edits) ? edits.map((item) => plainToInstance(getActionClass(item), item)) : edits, + ) + @ApiProperty({ + anyOf: Object.values(actionToClass).map((target) => ({ $ref: getSchemaPath(target) })), + description: 'List of edit actions to apply (crop, rotate, or mirror)', + }) + edits!: AssetEditActionItem[]; +} + +export class AssetEditsDto extends AssetEditActionListDto { + @ValidateUUID({ description: 'Asset ID to apply edits to' }) + assetId!: 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..0d73c19b20 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -8,24 +8,24 @@ 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 +35,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 +49,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 +74,46 @@ export class MemoryCreateDto extends MemoryBaseDto { }) data!: MemoryData; - @ValidateDate() + @ValidateDate({ description: 'Memory date' }) memoryAt!: Date; - @ValidateUUID({ optional: true, each: true }) + @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..5331db4e85 100644 --- a/server/src/dtos/notification.dto.ts +++ b/server/src/dtos/notification.dto.ts @@ -1,86 +1,115 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsString } from 'class-validator'; import { NotificationLevel, NotificationType } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, 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; + @ApiProperty({ description: 'Notification title' }) @IsString() title!: string; + @ApiPropertyOptional({ description: 'Notification description' }) @IsString() @Optional({ nullable: true }) description?: string | null; + @ApiPropertyOptional({ description: 'Additional notification data' }) @Optional({ nullable: true }) data?: any; - @ValidateDate({ optional: true, nullable: true }) + @ValidateDate({ optional: 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, description: 'Date when notification was read' }) readAt?: Date | null; } export class NotificationUpdateAllDto { - @ValidateUUID({ each: true, optional: true }) + @ValidateUUID({ each: true, optional: true, description: 'Notification IDs to update' }) ids!: string[]; - @ValidateDate({ optional: true, nullable: true }) + @ValidateDate({ optional: true, description: 'Date when notifications were read' }) readAt?: Date | null; } export class NotificationDeleteAllDto { - @ValidateUUID({ each: true }) + @ValidateUUID({ each: true, description: 'Notification IDs to delete' }) 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 a802bb1201..de1f1b28d4 100644 --- a/server/src/dtos/plugin.dto.ts +++ b/server/src/dtos/plugin.dto.ts @@ -1,3 +1,4 @@ +import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString } from 'class-validator'; import { PluginAction, PluginFilter } from 'src/database'; import { PluginContext as PluginContextType, PluginTriggerType } from 'src/enum'; @@ -5,50 +6,73 @@ import type { JSONSchema } from 'src/types/plugin-schema.types'; import { ValidateEnum } from 'src/validation'; export class PluginTriggerResponseDto { - @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType' }) + @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Trigger type' }) type!: PluginTriggerType; - @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType' }) + @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: PluginContextType, name: 'PluginContextType' }) + @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: PluginContextType, name: 'PluginContextType' }) + @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..59fddcc71c 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -1,107 +1,116 @@ -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'; -import { HistoryBuilder, Property } from 'src/decorators'; +import { HistoryBuilder } from 'src/decorators'; import { AlbumResponseDto } from 'src/dtos/album.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetOrder, AssetType, AssetVisibility } from 'src/enum'; import { Optional, 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, 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[]; + @ApiPropertyOptional({ type: 'number', description: 'Filter by rating', minimum: -1, maximum: 5 }) @Optional() @IsInt() @Max(5) @Min(-1) rating?: number; + @ApiPropertyOptional({ description: 'Filter by OCR text content' }) @IsString() @IsNotEmpty() @Optional() @@ -109,12 +118,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 +134,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 +214,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 +240,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 +289,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 82698ebddc..7b92f48e28 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,119 +1,145 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsString } from 'class-validator'; import { SharedLink } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; +import { HistoryBuilder } 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'; export class SharedLinkSearchDto { - @ValidateUUID({ optional: true }) + @ValidateUUID({ optional: true, description: 'Filter by album ID' }) albumId?: string; - @ValidateUUID({ optional: true }) - @Property({ history: new HistoryBuilder().added('v2.5.0') }) + @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, 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 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; + @ApiPropertyOptional({ description: 'Access token' }) 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; } 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 7f811af371..59d7d373f0 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -17,32 +17,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[]; } @@ -57,21 +60,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; @@ -81,129 +94,189 @@ 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 { + @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; + @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; @@ -211,87 +284,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; @@ -305,26 +418,31 @@ export class SyncAssetFaceV1 { boundingBoxX2!: number; @ApiProperty({ type: 'integer' }) boundingBoxY2!: number; + @ApiProperty({ description: 'Source type' }) sourceType!: string; } @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; } @@ -388,26 +506,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..231e6cc501 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({ 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({ 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..dfd474d885 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -132,7 +132,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..598798dc44 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,50 @@ 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, 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 +74,125 @@ 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, description: 'Avatar color' }) avatarColor?: UserAvatarColor | 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; - @PinCode({ optional: true, nullable: true, emptyToNull: true }) + @ApiPropertyOptional({ description: 'PIN code' }) + @PinCode({ optional: 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, 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 2dbff3b5e4..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,68 +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 }) + @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() @@ -75,31 +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' }) + @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/enum.ts b/server/src/enum.ts index b150cdbfb3..8f509754da 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -106,6 +106,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', @@ -128,6 +133,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', @@ -136,6 +146,8 @@ export enum Permission { FaceUpdate = 'face.update', FaceDelete = 'face.delete', + FolderRead = 'folder.read', + JobCreate = 'job.create', JobRead = 'job.read', @@ -150,6 +162,9 @@ export enum Permission { Maintenance = 'maintenance', + MapRead = 'map.read', + MapSearch = 'map.search', + MemoryCreate = 'memory.create', MemoryRead = 'memory.read', MemoryUpdate = 'memory.update', @@ -356,11 +371,7 @@ export enum ManualJobName { export enum AssetPathType { Original = 'original', - FullSize = 'fullsize', - Preview = 'preview', - Thumbnail = 'thumbnail', EncodedVideo = 'encoded_video', - Sidecar = 'sidecar', } export enum PersonPathType { @@ -371,7 +382,7 @@ export enum UserPathType { Profile = 'profile', } -export type PathType = AssetPathType | PersonPathType | UserPathType; +export type PathType = AssetFileType | AssetPathType | PersonPathType | UserPathType; export enum TranscodePolicy { All = 'all', @@ -555,6 +566,7 @@ export enum QueueName { BackupDatabase = 'backupDatabase', Ocr = 'ocr', Workflow = 'workflow', + Editor = 'editor', } export enum QueueJobStatus { @@ -573,6 +585,7 @@ export enum JobName { AssetDetectFaces = 'AssetDetectFaces', AssetDetectDuplicatesQueueAll = 'AssetDetectDuplicatesQueueAll', AssetDetectDuplicates = 'AssetDetectDuplicates', + AssetEditThumbnailGeneration = 'AssetEditThumbnailGeneration', AssetEncodeVideoQueueAll = 'AssetEncodeVideoQueueAll', AssetEncodeVideo = 'AssetEncodeVideo', AssetEmptyTrash = 'AssetEmptyTrash', @@ -684,12 +697,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 { @@ -836,6 +852,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..a8e3178a43 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,19 +35,18 @@ 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 { @@ -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 cf04c0ad12..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,10 +45,10 @@ 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', (event: ArgsOf<'AppRestart'>, ack?: (ok: 'ok') => void) => { + 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'); @@ -46,20 +56,40 @@ export class MaintenanceWebsocketRepository implements OnGatewayConnection, OnGa }); } + 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..72527e27c0 100644 --- a/server/src/maintenance/maintenance-worker.controller.ts +++ b/server/src/maintenance/maintenance-worker.controller.ts @@ -1,23 +1,114 @@ -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'; @Controller() export class MaintenanceWorkerController { - constructor(private service: MaintenanceWorkerService) {} + constructor( + private logger: LoggingRepository, + private service: MaintenanceWorkerService, + ) {} + /** + * {@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.service.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.service.downloadBackup(filename), this.logger); + } + + /** + * {@link _DatabaseBackupController.deleteDatabaseBackup} + */ + @Delete('admin/database-backups') + @MaintenanceRoute() + async deleteDatabaseBackup(@Body() dto: DatabaseBackupDeleteDto): Promise { + return this.service.deleteBackup(dto.backups); + } + + /** + * {@link _DatabaseBackupController.uploadDatabaseBackup} + */ + @Post('admin/database-backups/upload') + @MaintenanceRoute() + @UseInterceptors(FileInterceptor('file')) + uploadDatabaseBackup( + @UploadedFile() + file: Express.Multer.File, + ): Promise { + return this.service.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 +126,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..9fd8f38fcb 100644 --- a/server/src/maintenance/maintenance-worker.service.spec.ts +++ b/server/src/maintenance/maintenance-worker.service.spec.ts @@ -1,25 +1,51 @@ -import { UnauthorizedException } from '@nestjs/common'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { SignJWT } from 'jose'; -import { SystemMetadataKey } from 'src/enum'; +import { DateTime } from 'luxon'; +import { PassThrough, Readable } from 'node:stream'; +import { StorageCore } from 'src/cores/storage.core'; +import { MaintenanceAction, StorageFolder, 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 { automock, AutoMocked, getMocks, mockDuplex, mockSpawn, ServiceMocks } from 'test/utils'; + +function* mockData() { + yield ''; +} describe(MaintenanceWorkerService.name, () => { let sut: MaintenanceWorkerService; let mocks: ServiceMocks; - let maintenanceWorkerRepositoryMock: MaintenanceWebsocketRepository; + let maintenanceWebsocketRepositoryMock: AutoMocked; + let maintenanceHealthRepositoryMock: 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, + }); + 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, ); + + sut.mock({ + active: true, + action: MaintenanceAction.Start, + }); }); it('should work', () => { @@ -27,14 +53,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 +97,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 +125,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 +141,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 +248,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 +270,275 @@ 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); + + 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()); + }); + + 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 fail to restore invalid backup', async () => { + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'filename', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + active: true, + action: MaintenanceAction.RestoreDatabase, + error: 'Error: Invalid backup file format!', + task: 'error', + }); + }); + + it('should successfully run a backup', 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', + }, + ); + + expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalled(); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(3); + }); + + it('should fail if backup creation fails', async () => { + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('pg_dump', 1, '', 'error')); + + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'development-filename.sql', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + active: true, + action: MaintenanceAction.RestoreDatabase, + error: 'Error: pg_dump non-zero exit code (1)\nerror', + task: 'error', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenLastCalledWith( + 'MaintenanceStatusV1', + expect.any(String), + expect.objectContaining({ + task: 'error', + }), + ); + }); + + 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')); + + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'development-filename.sql', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + active: true, + action: MaintenanceAction.RestoreDatabase, + error: 'Error: psql non-zero exit code (1)\nerror', + task: 'error', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenLastCalledWith( + 'MaintenanceStatusV1', + expect.any(String), + expect.objectContaining({ + task: 'error', + }), + ); + }); + + it('should rollback if database migrations fail', async () => { + mocks.database.runMigrations.mockRejectedValue(new Error('Migrations Error')); + + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'development-filename.sql', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + active: true, + action: MaintenanceAction.RestoreDatabase, + error: 'Error: Migrations Error', + task: 'error', + }); + + 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')); + + await sut.runAction({ + action: MaintenanceAction.RestoreDatabase, + restoreBackupFilename: 'development-filename.sql', + }); + + expect(maintenanceWebsocketRepositoryMock.clientSend).toHaveBeenCalledWith('MaintenanceStatusV1', 'private', { + active: true, + action: MaintenanceAction.RestoreDatabase, + error: 'Error: Health Error', + task: 'error', + }); + + expect(maintenanceHealthRepositoryMock.checkApiHealth).toHaveBeenCalled(); + expect(mocks.process.spawnDuplexStream).toHaveBeenCalledTimes(4); + }); + }); + + /** + * Backups + */ + + 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('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', + }), + ); + }); + }); }); diff --git a/server/src/maintenance/maintenance-worker.service.ts b/server/src/maintenance/maintenance-worker.service.ts index c03231c274..6415693733 100644 --- a/server/src/maintenance/maintenance-worker.service.ts +++ b/server/src/maintenance/maintenance-worker.service.ts @@ -4,19 +4,41 @@ 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 { type DatabaseBackupService as _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 { + deleteDatabaseBackup, + downloadDatabaseBackup, + listDatabaseBackups, + restoreDatabaseBackup, + uploadDatabaseBackup, +} from 'src/utils/database-backups'; +import { ImmichFileResponse } from 'src/utils/file'; +import { createMaintenanceLoginUrl, detectPriorInstall } from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; /** @@ -24,16 +46,54 @@ 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, ) { 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 +115,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 +161,99 @@ 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'; + } + + /** + * {@link _DatabaseBackupService.listBackups} + */ + async listBackups(): Promise<{ backups: { filename: string; filesize: number }[] }> { + const backups = await listDatabaseBackups(this.backupRepos); + return { backups }; + } + + /** + * {@link _DatabaseBackupService.deleteBackup} + */ + async deleteBackup(files: string[]): Promise { + return deleteDatabaseBackup(this.backupRepos, files); + } + + /** + * {@link _DatabaseBackupService.uploadBackup} + */ + async uploadBackup(file: Express.Multer.File): Promise { + return uploadDatabaseBackup(this.backupRepos, file); + } + + /** + * {@link _DatabaseBackupService.downloadBackup} + */ + downloadBackup(fileName: string): ImmichFileResponse { + return downloadDatabaseBackup(fileName); + } + + 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 +265,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 +276,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 restoreDatabaseBackup(this.backupRepos, 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/queries/asset.edit.repository.sql b/server/src/queries/asset.edit.repository.sql new file mode 100644 index 0000000000..0cf62882db --- /dev/null +++ b/server/src/queries/asset.edit.repository.sql @@ -0,0 +1,19 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- AssetEditRepository.replaceAll +begin +delete from "asset_edit" +where + "assetId" = $1 +rollback + +-- AssetEditRepository.getAll +select + "action", + "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 ae2b5110c2..50f2c193fc 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 @@ -99,13 +87,28 @@ 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" + ) 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" from "asset" inner join "asset_job_status" on "asset_job_status"."assetId" = "asset"."id" @@ -113,8 +116,22 @@ where "asset"."deletedAt" is null and "asset"."visibility" != $1 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 "asset_file"."type" = $2 + ) + or not exists ( + select + from + "asset_file" + where + "assetId" = "asset"."id" + and "asset_file"."type" = $3 + ) or "asset"."thumbhash" is null ) @@ -131,7 +148,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -160,19 +178,36 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited", + "asset_file"."isProgressive" 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 +226,8 @@ select "asset"."originalPath", "asset"."ownerId", "asset"."type", + "asset"."width", + "asset"."height", ( select coalesce(json_agg(agg), '[]') @@ -203,6 +240,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 +251,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 +277,8 @@ where select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -266,7 +306,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 +334,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -326,7 +374,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -402,6 +451,7 @@ select where "asset_face"."assetId" = "asset"."id" and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true ) as agg ) as "faces", ( @@ -412,7 +462,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -504,7 +555,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -543,7 +595,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -591,7 +644,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 27e40139e1..0f3a458c35 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", @@ -117,8 +134,7 @@ with "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 ( @@ -182,6 +198,7 @@ select where "asset_face"."assetId" = "asset"."id" and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true ) as agg ) as "faces", ( @@ -268,7 +285,8 @@ select select "asset_file"."id", "asset_file"."path", - "asset_file"."type" + "asset_file"."type", + "asset_file"."isEdited" from "asset_file" where @@ -383,14 +401,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, @@ -570,3 +584,40 @@ where and "libraryId" = $2::uuid and "isExternal" = $3 ) + +-- AssetRepository.getForOriginal +select + "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" = $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 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..356f5af8f6 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 @@ -229,6 +232,7 @@ from and "asset"."deletedAt" is null where "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true -- PersonRepository.getNumberOfPeople select @@ -250,6 +254,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 +265,7 @@ where and "asset"."deletedAt" is null ) ) - and "person"."ownerId" = $2 + and "person"."ownerId" = $3 -- PersonRepository.refreshFaces with @@ -321,6 +326,7 @@ from where "asset_face"."personId" = $1 and "asset_face"."deletedAt" is null + and "asset_face"."isVisible" is true -- PersonRepository.getLatestFaceDate select 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..f817ad57b3 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" @@ -536,6 +548,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 +753,9 @@ select "asset"."livePhotoVideoId", "asset"."stackId", "asset"."libraryId", + "asset"."width", + "asset"."height", + "asset"."isEdited", "asset"."updateId" from "asset" as "asset" @@ -789,6 +805,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/repositories/asset-edit.repository.ts b/server/src/repositories/asset-edit.repository.ts new file mode 100644 index 0000000000..088cb1ccff --- /dev/null +++ b/server/src/repositories/asset-edit.repository.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { Kysely } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { AssetEditActionItem } 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(['action', 'parameters']) + .execute() as Promise; + } + + return []; + }); + } + + @GenerateSql({ + params: [DummyValue.UUID], + }) + getAll(assetId: string): Promise { + return this.db + .selectFrom('asset_edit') + .select(['action', 'parameters']) + .where('assetId', '=', assetId) + .orderBy('sequence', 'asc') + .execute() as Promise; + } +} diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index 8beb053aac..1608f7b6f6 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -11,6 +11,7 @@ import { asUuid, toJson, withDefaultVisibility, + withEdits, withExif, withExifInner, withFaces, @@ -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(); @@ -72,6 +64,7 @@ export class AssetJobRepository { .selectFrom('asset') .select(['asset.id', 'asset.thumbhash']) .select(withFiles) + .select(withEdits) .where('asset.deletedAt', 'is', null) .where('asset.visibility', '!=', AssetVisibility.Hidden) .$if(!force, (qb) => @@ -80,8 +73,22 @@ export class AssetJobRepository { .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), + eb.not((eb) => + eb.exists((qb) => + qb + .selectFrom('asset_file') + .whereRef('assetId', '=', 'asset.id') + .where('asset_file.type', '=', AssetFileType.Preview), + ), + ), + eb.not((eb) => + eb.exists((qb) => + qb + .selectFrom('asset_file') + .whereRef('assetId', '=', 'asset.id') + .where('asset_file.type', '=', AssetFileType.Thumbnail), + ), + ), eb('asset.thumbhash', 'is', null), ]), ), @@ -112,7 +119,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 +171,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 +223,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(); diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index e1d16b8a6a..1a060c4715 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -20,6 +20,7 @@ import { truncatedDate, unnest, withDefaultVisibility, + withEdits, withExif, withFaces, withFacesAndPeople, @@ -112,6 +113,7 @@ interface GetByIdsRelations { smartSearch?: boolean; stack?: { assets?: boolean }; tags?: boolean; + edits?: boolean; } const distinctLocked = (eb: ExpressionBuilder, columns: T) => @@ -176,6 +178,7 @@ export class AssetRepository { bitsPerSample: ref('bitsPerSample'), rating: ref('rating'), fps: ref('fps'), + tags: ref('tags'), lockedProperties: lockedPropertiesBehavior === 'append' ? distinctLocked(eb, exif.lockedProperties ?? null) @@ -221,6 +224,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; @@ -237,8 +251,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], @@ -347,7 +359,6 @@ export class AssetRepository { .selectFrom('asset') .selectAll('asset') .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) @@ -472,7 +483,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') @@ -509,6 +523,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(); } @@ -536,10 +551,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 { @@ -696,11 +712,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), ) @@ -887,31 +901,34 @@ export class AssetRepository { .execute(); } - async upsertFile(file: Pick, 'assetId' | 'path' | 'type'>): Promise { - const value = { ...file, assetId: asUuid(file.assetId) }; + async upsertFile( + file: Pick, 'assetId' | 'path' | 'type' | 'isEdited' | 'isProgressive'>, + ): 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, 'assetId' | 'path' | 'type' | 'isEdited' | 'isProgressive'>[], + ): 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'), })), ) .execute(); @@ -990,4 +1007,47 @@ export class AssetRepository { return count; } + + @GenerateSql({ params: [DummyValue.UUID, true] }) + async getForOriginal(id: string, isEdited: boolean) { + return this.db + .selectFrom('asset') + .select('originalFileName') + .where('asset.id', '=', id) + .$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') + .executeTakeFirstOrThrow(); + } + + @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(); + } } 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/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 a8e96709ff..33025e73cf 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) => @@ -138,21 +140,49 @@ export class MediaRepository { } } - decodeImage(input: string | Buffer, options: DecodeToBufferOptions) { - return this.getImageDecodingPipeline(input, options).raw().toBuffer({ resolveWithObject: true }); + async decodeImage(input: string | Buffer, options: DecodeToBufferOptions) { + const pipeline = await this.getImageDecodingPipeline(input, options); + return pipeline.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); } - 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', @@ -175,8 +205,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) { @@ -186,14 +216,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)); } 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..b03112821b 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { ExpressionBuilder, Insertable, Kysely, NotNull, 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(); } @@ -350,6 +357,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 +376,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 @@ -495,6 +504,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 +549,37 @@ 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(); + } + }); + } } 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/storage.repository.ts b/server/src/repositories/storage.repository.ts index e901273b57..7345dfef5b 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); diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index 437e32da16..511d7b589f 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -483,6 +483,7 @@ class AssetFaceSync extends BaseSync { ]) .leftJoin('asset', 'asset.id', 'asset_face.assetId') .where('asset.ownerId', '=', options.userId) + .where('asset_face.isVisible', '=', true) .stream(); } } diff --git a/server/src/repositories/websocket.repository.ts b/server/src/repositories/websocket.repository.ts index d87bf76351..bfed556895 100644 --- a/server/src/repositories/websocket.repository.ts +++ b/server/src/repositories/websocket.repository.ts @@ -37,6 +37,7 @@ export interface ClientEventMap { AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }]; AppRestartV1: [AppRestartEvent]; + AssetEditReadyV1: [{ asset: SyncAssetV1 }]; } export type AuthFn = (client: Socket) => Promise; diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index 385db37cf8..d7dabfef4c 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -255,3 +255,34 @@ 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 + `, +}); diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 9e206826e6..59c9f53d1a 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -28,6 +28,7 @@ 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 { 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'; @@ -86,6 +87,7 @@ export class ImmichDatabase { AlbumTable, ApiKeyTable, AssetAuditTable, + AssetEditTable, AssetFaceTable, AssetFaceAuditTable, AssetMetadataTable, @@ -179,6 +181,7 @@ export interface DB { asset: AssetTable; asset_audit: AssetAuditTable; + asset_edit: AssetEditTable; asset_exif: AssetExifTable; asset_face: AssetFaceTable; asset_face_audit: AssetFaceAuditTable; 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/tables/asset-edit.table.ts b/server/src/schema/tables/asset-edit.table.ts new file mode 100644 index 0000000000..886b62dc0b --- /dev/null +++ b/server/src/schema/tables/asset-edit.table.ts @@ -0,0 +1,39 @@ +import { AssetEditAction, AssetEditActionParameter } from 'src/dtos/editing.dto'; +import { asset_edit_delete, asset_edit_insert } from 'src/schema/functions'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { + AfterDeleteTrigger, + AfterInsertTrigger, + Column, + ForeignKeyColumn, + Generated, + PrimaryGeneratedColumn, + Table, + Unique, +} from 'src/sql-tools'; + +@Table('asset_edit') +@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', +}) +@Unique({ columns: ['assetId', 'sequence'] }) +export class AssetEditTable { + @PrimaryGeneratedColumn() + id!: Generated; + + @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) + assetId!: string; + + @Column() + action!: T; + + @Column({ type: 'jsonb' }) + parameters!: AssetEditActionParameter[T]; + + @Column({ type: 'integer' }) + sequence!: number; +} diff --git a/server/src/schema/tables/asset-exif.table.ts b/server/src/schema/tables/asset-exif.table.ts index 346098a72c..9dacb547cf 100644 --- a/server/src/schema/tables/asset-exif.table.ts +++ b/server/src/schema/tables/asset-exif.table.ts @@ -93,6 +93,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.table.ts b/server/src/schema/tables/asset-face.table.ts index 5041d945e2..8b156f2a17 100644 --- a/server/src/schema/tables/asset-face.table.ts +++ b/server/src/schema/tables/asset-face.table.ts @@ -78,4 +78,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..73b5171a47 100644 --- a/server/src/schema/tables/asset-file.table.ts +++ b/server/src/schema/tables/asset-file.table.ts @@ -14,7 +14,7 @@ import { } from 'src/sql-tools'; @Table('asset_file') -@Unique({ columns: ['assetId', 'type'] }) +@Unique({ columns: ['assetId', 'type', 'isEdited'] }) @UpdatedAtTrigger('asset_file_updatedAt') export class AssetFileTable { @PrimaryGeneratedColumn() @@ -37,4 +37,10 @@ export class AssetFileTable { @UpdateIdColumn({ index: true }) updateId!: Generated; + + @Column({ type: 'boolean', default: false }) + isEdited!: Generated; + + @Column({ type: 'boolean', default: false }) + isProgressive!: 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..62194825e5 100644 --- a/server/src/schema/tables/asset-job-status.table.ts +++ b/server/src/schema/tables/asset-job-status.table.ts @@ -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-ocr.table.ts b/server/src/schema/tables/asset-ocr.table.ts index 6ab159b531..b9b0838cbe 100644 --- a/server/src/schema/tables/asset-ocr.table.ts +++ b/server/src/schema/tables/asset-ocr.table.ts @@ -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..0b3da710ac 100644 --- a/server/src/schema/tables/asset.table.ts +++ b/server/src/schema/tables/asset.table.ts @@ -137,4 +137,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/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts index 8d48b47f1e..14544f454f 100644 --- a/server/src/services/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -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..492ee9c0fd 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -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..0bcb87e2f4 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -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,19 +500,124 @@ 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'])); - - await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(NotFoundException); - - expect(mocks.asset.getById).toHaveBeenCalledWith('asset-1', { files: true }); - }); - it('should download a file', async () => { mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - mocks.asset.getById.mockResolvedValue(assetStub.image); + mocks.asset.getForOriginal.mockResolvedValue(assetStub.image); - await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).resolves.toEqual( + await expect(sut.downloadOriginal(authStub.admin, 'asset-1', {})).resolves.toEqual( + new ImmichFileResponse({ + path: '/original/path.jpg', + fileName: 'asset-id.jpg', + contentType: 'image/jpeg', + cacheControl: CacheControl.PrivateWithCache, + }), + ); + }); + + it('should download edited file by default when edits exist', async () => { + const editedAsset = { + ...assetStub.withCropEdit, + files: [ + ...assetStub.withCropEdit.files, + { + id: 'edited-file', + type: AssetFileType.FullSize, + path: '/uploads/user-id/fullsize/edited.jpg', + isEdited: true, + }, + ], + }; + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + mocks.asset.getForOriginal.mockResolvedValue({ + ...editedAsset, + editedPath: '/uploads/user-id/fullsize/edited.jpg', + }); + + await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: true })).resolves.toEqual( + new ImmichFileResponse({ + path: '/uploads/user-id/fullsize/edited.jpg', + fileName: 'asset-id.jpg', + contentType: 'image/jpeg', + cacheControl: CacheControl.PrivateWithCache, + }), + ); + }); + + it('should download edited file when edited=true', async () => { + const editedAsset = { + ...assetStub.withCropEdit, + files: [ + ...assetStub.withCropEdit.files, + { + id: 'edited-file', + type: AssetFileType.FullSize, + path: '/uploads/user-id/fullsize/edited.jpg', + isEdited: true, + }, + ], + }; + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + mocks.asset.getForOriginal.mockResolvedValue({ + ...editedAsset, + editedPath: '/uploads/user-id/fullsize/edited.jpg', + }); + + await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: true })).resolves.toEqual( + new ImmichFileResponse({ + path: '/uploads/user-id/fullsize/edited.jpg', + fileName: 'asset-id.jpg', + contentType: 'image/jpeg', + cacheControl: CacheControl.PrivateWithCache, + }), + ); + }); + + it('should not return the unedited version if requested using a shared link', async () => { + const editedAsset = { + ...assetStub.withCropEdit, + files: [ + ...assetStub.withCropEdit.files, + { + id: 'edited-file', + type: AssetFileType.FullSize, + path: '/uploads/user-id/fullsize/edited.jpg', + isEdited: true, + }, + ], + }; + mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id])); + mocks.asset.getForOriginal.mockResolvedValue({ + ...editedAsset, + editedPath: '/uploads/user-id/fullsize/edited.jpg', + }); + + await expect(sut.downloadOriginal(authStub.adminSharedLink, 'asset-id', { edited: false })).resolves.toEqual( + new ImmichFileResponse({ + path: '/uploads/user-id/fullsize/edited.jpg', + fileName: 'asset-id.jpg', + contentType: 'image/jpeg', + cacheControl: CacheControl.PrivateWithCache, + }), + ); + }); + + it('should download original file when edited=false', async () => { + const editedAsset = { + ...assetStub.withCropEdit, + files: [ + ...assetStub.withCropEdit.files, + { + id: 'edited-file', + type: AssetFileType.FullSize, + path: '/uploads/user-id/fullsize/edited.jpg', + isEdited: true, + }, + ], + }; + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + mocks.asset.getForOriginal.mockResolvedValue(editedAsset); + + await expect(sut.downloadOriginal(authStub.admin, 'asset-1', { edited: false })).resolves.toEqual( new ImmichFileResponse({ path: '/original/path.jpg', fileName: 'asset-id.jpg', @@ -532,52 +637,9 @@ 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, - }, - ], - }); + mocks.asset.getForThumbnail.mockResolvedValue({ ...assetStub.image, path: '/path/to/preview.jpg' }); await expect( sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }), @@ -593,7 +655,7 @@ describe(AssetMediaService.name, () => { it('should get preview file', async () => { mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue({ ...assetStub.image }); + mocks.asset.getForThumbnail.mockResolvedValue({ ...assetStub.image, path: '/uploads/user-id/thumbs/path.jpg' }); await expect( sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.PREVIEW }), ).resolves.toEqual( @@ -608,7 +670,7 @@ describe(AssetMediaService.name, () => { it('should get thumbnail file', async () => { mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); - mocks.asset.getById.mockResolvedValue({ ...assetStub.image }); + mocks.asset.getForThumbnail.mockResolvedValue({ ...assetStub.image, path: '/uploads/user-id/webp/path.ext' }); await expect( sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }), ).resolves.toEqual( @@ -619,6 +681,86 @@ describe(AssetMediaService.name, () => { fileName: 'asset-id_thumbnail.ext', }), ); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, false); + }); + + it('should get original thumbnail by default', async () => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ + ...assetStub.image, + path: '/uploads/user-id/thumbs/original-thumbnail.jpg', + }); + await expect( + sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL }), + ).resolves.toEqual( + new ImmichFileResponse({ + path: '/uploads/user-id/thumbs/original-thumbnail.jpg', + cacheControl: CacheControl.PrivateWithCache, + contentType: 'image/jpeg', + fileName: 'asset-id_thumbnail.jpg', + }), + ); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, false); + }); + + it('should get edited thumbnail when edited=true', async () => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ + ...assetStub.image, + path: '/uploads/user-id/thumbs/edited-thumbnail.jpg', + }); + await expect( + sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL, edited: true }), + ).resolves.toEqual( + new ImmichFileResponse({ + path: '/uploads/user-id/thumbs/edited-thumbnail.jpg', + cacheControl: CacheControl.PrivateWithCache, + contentType: 'image/jpeg', + fileName: 'asset-id_thumbnail.jpg', + }), + ); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, true); + }); + + it('should get original thumbnail when edited=false', async () => { + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ + ...assetStub.image, + path: '/uploads/user-id/thumbs/original-thumbnail.jpg', + }); + await expect( + sut.viewThumbnail(authStub.admin, assetStub.image.id, { size: AssetMediaSize.THUMBNAIL, edited: false }), + ).resolves.toEqual( + new ImmichFileResponse({ + path: '/uploads/user-id/thumbs/original-thumbnail.jpg', + cacheControl: CacheControl.PrivateWithCache, + contentType: 'image/jpeg', + fileName: 'asset-id_thumbnail.jpg', + }), + ); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, false); + }); + + it('should not return the unedited version if requested using a shared link', async () => { + mocks.access.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id])); + mocks.asset.getForThumbnail.mockResolvedValue({ + ...assetStub.image, + path: '/uploads/user-id/thumbs/edited-thumbnail.jpg', + }); + await expect( + sut.viewThumbnail(authStub.adminSharedLink, assetStub.image.id, { + size: AssetMediaSize.THUMBNAIL, + edited: true, + }), + ).resolves.toEqual( + new ImmichFileResponse({ + path: '/uploads/user-id/thumbs/edited-thumbnail.jpg', + cacheControl: CacheControl.PrivateWithCache, + contentType: 'image/jpeg', + fileName: 'asset-id_thumbnail.jpg', + }), + ); + expect(mocks.asset.getForThumbnail).toHaveBeenCalledWith(assetStub.image.id, AssetFileType.Thumbnail, true); }); }); @@ -631,22 +773,15 @@ 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 () => { + it('should throw an error if the video asset could not be found', async () => { mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.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); - }); - 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); + mocks.asset.getForVideo.mockResolvedValue(assetStub.hasEncodedVideo); await expect(sut.playbackVideo(authStub.admin, assetStub.hasEncodedVideo.id)).resolves.toEqual( new ImmichFileResponse({ @@ -659,7 +794,7 @@ 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); + mocks.asset.getForVideo.mockResolvedValue(assetStub.video); await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual( new ImmichFileResponse({ diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 5683c6ae15..020bda4df7 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,10 +265,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; @@ -463,13 +477,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..eca49bc14e 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -2,7 +2,8 @@ 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 { 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'; @@ -704,6 +705,7 @@ describe(AssetService.name, () => { mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); mocks.ocr.getByAssetId.mockResolvedValue([ocr1, ocr2]); + mocks.asset.getById.mockResolvedValue(assetStub.image); await expect(sut.getOcr(authStub.admin, 'asset-1')).resolves.toEqual([ocr1, ocr2]); @@ -718,7 +720,7 @@ describe(AssetService.name, () => { it('should return empty array when no OCR data exists', async () => { mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); mocks.ocr.getByAssetId.mockResolvedValue([]); - + mocks.asset.getById.mockResolvedValue(assetStub.image); await expect(sut.getOcr(authStub.admin, 'asset-1')).resolves.toEqual([]); expect(mocks.ocr.getByAssetId).toHaveBeenCalledWith('asset-1'); @@ -776,4 +778,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 1e776bd256..066084ed45 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -4,7 +4,7 @@ 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, @@ -21,13 +21,32 @@ import { mapStats, } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { AssetEditAction, AssetEditActionCrop, AssetEditActionListDto, AssetEditsDto } from 'src/dtos/editing.dto'; import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; -import { AssetFileType, AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum'; +import { + AssetFileType, + AssetStatus, + AssetType, + AssetVisibility, + JobName, + JobStatus, + Permission, + QueueName, +} from 'src/enum'; 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 { transformOcrBoundingBox } from 'src/utils/transform'; @Injectable() export class AssetService extends BaseService { @@ -62,6 +81,7 @@ export class AssetService extends BaseService { owner: true, faces: { person: true }, stack: { assets: true }, + edits: true, tags: true, }); @@ -92,7 +112,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) { @@ -339,11 +359,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) } }); @@ -372,16 +400,46 @@ 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.getById(id, { exifInfo: true, edits: true }); + + if (!asset || !asset.exifInfo || !asset.edits) { + throw new BadRequestException('Asset not found'); + } + + const dimensions = getDimensions(asset.exifInfo); + + 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); } @@ -478,4 +536,83 @@ 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: AssetEditActionListDto): Promise { + await this.requireAccess({ auth, permission: Permission.AssetEditCreate, ids: [id] }); + + const asset = await this.assetRepository.getById(id, { exifInfo: true }); + 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'); + } + + const cropIndex = dto.edits.findIndex((e) => e.action === AssetEditAction.Crop); + if (cropIndex > 0) { + throw new BadRequestException('Crop action must be the first edit action'); + } + + const crop = cropIndex === -1 ? null : (dto.edits[cropIndex] as AssetEditActionCrop); + if (crop) { + // check that crop parameters will not go out of bounds + const { width: assetWidth, height: assetHeight } = getDimensions(asset.exifInfo!); + + 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, dto.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/backup.service.spec.ts b/server/src/services/backup.service.spec.ts index 9e25fbaf2e..ea80dd5759 100644 --- a/server/src/services/backup.service.spec.ts +++ b/server/src/services/backup.service.spec.ts @@ -5,7 +5,7 @@ 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 { mockDuplex, mockSpawn, newTestService, ServiceMocks } from 'test/utils'; import { describe } from 'vitest'; describe(BackupService.name, () => { @@ -147,6 +147,7 @@ describe(BackupService.name, () => { 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); @@ -165,7 +166,7 @@ describe(BackupService.name, () => { ({ sut, mocks } = newTestService(BackupService, { config: configMock })); 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); @@ -174,14 +175,16 @@ describe(BackupService.name, () => { await sut.handleBackupDatabase(); - expect(mocks.process.spawn).toHaveBeenCalled(); - const call = mocks.process.spawn.mock.calls[0]; + expect(mocks.process.spawnDuplexStream).toHaveBeenCalled(); + const call = mocks.process.spawnDuplexStream.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'); + expect(args).toMatchInlineSnapshot(` + [ + "postgresql://postgres:pwd@host:5432/immich?sslmode=require", + "--clean", + "--if-exists", + ] + `); }); it('should run a database backup successfully', async () => { @@ -196,21 +199,21 @@ describe(BackupService.name, () => { 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 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.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1'); + 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.spawn.mockReturnValueOnce(mockSpawn(0, 'data', '')); - mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('Gzip failed with code 1'); + 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 () => { @@ -226,9 +229,9 @@ describe(BackupService.name, () => { }); it('should ignore unlink failing and still return failed job status', async () => { - mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error')); + mocks.process.spawnDuplexStream.mockReturnValueOnce(mockDuplex('pg_dump', 1, '', 'error')); mocks.storage.unlink.mockRejectedValue(new Error('error')); - await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1'); + await expect(sut.handleBackupDatabase()).rejects.toThrow('pg_dump non-zero exit code (1)'); expect(mocks.storage.unlink).toHaveBeenCalled(); }); @@ -242,12 +245,12 @@ describe(BackupService.name, () => { ${'17.15.1'} | ${17} ${'18.0.0'} | ${18} `( - `should use pg_dumpall $expectedVersion with postgres version $postgresVersion`, + `should use pg_dump $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(mocks.process.spawnDuplexStream).toHaveBeenCalledWith( + `/usr/lib/postgresql/${expectedVersion}/bin/pg_dump`, expect.any(Array), expect.any(Object), ); diff --git a/server/src/services/backup.service.ts b/server/src/services/backup.service.ts index 2ff3e5dd3e..637e968929 100644 --- a/server/src/services/backup.service.ts +++ b/server/src/services/backup.service.ts @@ -1,13 +1,16 @@ 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 { + createDatabaseBackup, + isFailedDatabaseBackupName, + isValidDatabaseRoutineBackupName, + UnsupportedPostgresError, +} from 'src/utils/database-backups'; import { handlePromiseError } from 'src/utils/misc'; @Injectable() @@ -53,16 +56,11 @@ export class BackupService extends BaseService { 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; - }) + .filter((filename) => isValidDatabaseRoutineBackupName(filename)) .toSorted() .toReversed(); + const failedBackups = files.filter((filename) => isFailedDatabaseBackupName(filename)); const toDelete = backups.slice(config.keepLastAmount); toDelete.push(...failedBackups); @@ -75,123 +73,27 @@ export class BackupService extends BaseService { @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', '')); + await createDatabaseBackup(this.backupRepos); } 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}`)); + if (error instanceof UnsupportedPostgresError) { + return JobStatus.Failed; + } + throw error; } - this.logger.log(`Database Backup Success`); await this.cleanupDatabaseBackups(); return JobStatus.Success; } + + private get backupRepos() { + return { + logger: this.logger, + storage: this.storageRepository, + config: this.configRepository, + process: this.processRepository, + database: this.databaseRepository, + }; + } } 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 f4f14c3e68..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'; @@ -95,7 +95,14 @@ describe(CliService.name, () => { }); 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, }); @@ -109,7 +116,14 @@ 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, @@ -133,13 +147,22 @@ describe(CliService.name, () => { 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 8d2f1b0e99..ce62f98aa1 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -3,7 +3,7 @@ import { isAbsolute } 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 } from 'src/utils/maintenance'; import { getExternalDomain } from 'src/utils/misc'; @@ -86,6 +86,9 @@ export class CliService extends BaseService { await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: true, secret, + action: { + action: MaintenanceAction.Start, + }, }); await this.appRepository.sendOneShotAppRestart({ 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..4d68b02325 --- /dev/null +++ b/server/src/services/database-backup.service.spec.ts @@ -0,0 +1,83 @@ +import { BadRequestException } from '@nestjs/common'; +import { DateTime } from 'luxon'; +import { StorageCore } from 'src/cores/storage.core'; +import { StorageFolder } from 'src/enum'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; +import { MaintenanceService } from 'src/services/maintenance.service'; +import { newTestService, ServiceMocks } from 'test/utils'; + +describe(MaintenanceService.name, () => { + let sut: DatabaseBackupService; + let mocks: ServiceMocks; + + beforeEach(() => { + ({ sut, mocks } = newTestService(DatabaseBackupService)); + }); + + it('should work', () => { + expect(sut).toBeDefined(); + }); + + 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('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', + }), + ); + }); + }); +}); diff --git a/server/src/services/database-backup.service.ts b/server/src/services/database-backup.service.ts new file mode 100644 index 0000000000..542e961b43 --- /dev/null +++ b/server/src/services/database-backup.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; +import { DatabaseBackupListResponseDto } from 'src/dtos/database-backup.dto'; +import { BaseService } from 'src/services/base.service'; +import { + deleteDatabaseBackup, + downloadDatabaseBackup, + listDatabaseBackups, + uploadDatabaseBackup, +} from 'src/utils/database-backups'; +import { ImmichFileResponse } from 'src/utils/file'; + +/** + * This service is available outside of maintenance mode to manage maintenance mode + */ +@Injectable() +export class DatabaseBackupService extends BaseService { + async listBackups(): Promise { + const backups = await listDatabaseBackups(this.backupRepos); + return { backups }; + } + + deleteBackup(files: string[]): Promise { + return deleteDatabaseBackup(this.backupRepos, files); + } + + async uploadBackup(file: Express.Multer.File): Promise { + return uploadDatabaseBackup(this.backupRepos, file); + } + + downloadBackup(fileName: string): ImmichFileResponse { + return downloadDatabaseBackup(fileName); + } + + private get backupRepos() { + return { + logger: this.logger, + storage: this.storageRepository, + config: this.configRepository, + process: this.processRepository, + database: this.databaseRepository, + }; + } +} diff --git a/server/src/services/index.ts b/server/src/services/index.ts index eeb8424048..2c2fb995c8 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -9,6 +9,7 @@ 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'; @@ -59,6 +60,7 @@ export const services = [ AuthAdminService, BackupService, CliService, + DatabaseBackupService, DatabaseService, DownloadService, DuplicateService, diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index b57a203788..2a47745a6c 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -96,6 +96,38 @@ export class JobService extends BaseService { break; } + case JobName.AssetEditThumbnailGeneration: { + const asset = await this.assetRepository.getById(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, + }, + }); + } + + break; + } + case JobName.AssetGenerateThumbnails: { if (!item.data.notify && item.data.source !== 'upload') { break; @@ -141,6 +173,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/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 0f5fa06957..8e711ef380 100644 --- a/server/src/services/maintenance.service.ts +++ b/server/src/services/maintenance.service.ts @@ -1,11 +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'; /** @@ -19,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 { @@ -31,6 +57,20 @@ 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(event: ArgOf<'AppRestart'>, ack?: (ok: 'ok') => void): void { this.logger.log(`Restarting due to event... ${JSON.stringify(event)}`); diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 8617930534..fa2607faa9 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -18,13 +18,17 @@ 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 { assetStub, previewFile } from 'test/fixtures/asset.stub'; import { faceStub } from 'test/fixtures/face.stub'; import { probeStub } from 'test/fixtures/media.stub'; import { personStub, personThumbnailStub } from 'test/fixtures/person.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; 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; @@ -160,6 +164,42 @@ describe(MediaService.name, () => { expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); }); + + it('should queue assets with edits but missing edited thumbnails', async () => { + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.withCropEdit])); + mocks.person.getAll.mockReturnValue(makeStream()); + await sut.handleQueueGenerateThumbnails({ force: false }); + + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(false); + expect(mocks.job.queueAll).toHaveBeenCalledWith([ + { + name: JobName.AssetEditThumbnailGeneration, + data: { id: assetStub.withCropEdit.id }, + }, + ]); + + expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); + }); + + it('should queue both regular and edited thumbnails for assets with edits when force is true', async () => { + mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([assetStub.withCropEdit])); + mocks.person.getAll.mockReturnValue(makeStream()); + await sut.handleQueueGenerateThumbnails({ force: true }); + + expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith(true); + expect(mocks.job.queueAll).toHaveBeenCalledWith([ + { + name: JobName.AssetGenerateThumbnails, + data: { id: assetStub.withCropEdit.id }, + }, + { + name: JobName.AssetEditThumbnailGeneration, + data: { id: assetStub.withCropEdit.id }, + }, + ]); + + expect(mocks.person.getAll).toHaveBeenCalledWith(undefined); + }); }); describe('handleQueueMigration', () => { @@ -201,37 +241,33 @@ describe(MediaService.name, () => { await expect(sut.handleAssetMigration({ id: assetStub.image.id })).resolves.toBe(JobStatus.Success); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: assetStub.image.id, - pathType: AssetPathType.FullSize, + pathType: AssetFileType.FullSize, oldPath: '/uploads/user-id/fullsize/path.webp', - newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-fullsize.jpeg'), + newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_fullsize.jpeg'), }); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: assetStub.image.id, - pathType: AssetPathType.Preview, + pathType: AssetFileType.Preview, oldPath: '/uploads/user-id/thumbs/path.jpg', - newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-preview.jpeg'), + newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id_preview.jpeg'), }); expect(mocks.move.create).toHaveBeenCalledWith({ entityId: assetStub.image.id, - pathType: AssetPathType.Thumbnail, + pathType: AssetFileType.Thumbnail, oldPath: '/uploads/user-id/webp/path.ext', - newPath: expect.stringContaining('/data/thumbs/user-id/as/se/asset-id-thumbnail.webp'), + newPath: expect.stringContaining('/data/thumbs/user-id/as/se/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' @@ -281,7 +317,12 @@ describe(MediaService.name, () => { await sut.handleGenerateThumbnails({ id: assetStub.image.id }); - expect(mocks.storage.unlink).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg'); + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { + files: expect.arrayContaining([previewFile.path]), + }, + }); }); it('should generate P3 thumbnails for a wide gamut image', async () => { @@ -311,8 +352,10 @@ describe(MediaService.name, () => { format: ImageFormat.Jpeg, size: 1440, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -323,8 +366,10 @@ describe(MediaService.name, () => { format: ImageFormat.Webp, size: 250, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -334,6 +379,7 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, processInvalidImages: false, raw: rawInfo, + edits: [], }); expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ @@ -341,11 +387,15 @@ describe(MediaService.name, () => { assetId: 'asset-id', type: AssetFileType.Preview, path: expect.any(String), + isEdited: false, + isProgressive: false, }, { assetId: 'asset-id', type: AssetFileType.Thumbnail, path: expect.any(String), + isEdited: false, + isProgressive: false, }, ]); expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer }); @@ -377,11 +427,15 @@ describe(MediaService.name, () => { assetId: 'asset-id', type: AssetFileType.Preview, path: expect.any(String), + isEdited: false, + isProgressive: false, }, { assetId: 'asset-id', type: AssetFileType.Thumbnail, path: expect.any(String), + isEdited: false, + isProgressive: false, }, ]); }); @@ -412,11 +466,15 @@ describe(MediaService.name, () => { assetId: 'asset-id', type: AssetFileType.Preview, path: expect.any(String), + isEdited: false, + isProgressive: false, }, { assetId: 'asset-id', type: AssetFileType.Thumbnail, path: expect.any(String), + isEdited: false, + isProgressive: false, }, ]); }); @@ -504,8 +562,8 @@ describe(MediaService.name, () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); 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/user-id/as/se/asset-id_preview.${format}`; + const thumbnailPath = `/data/thumbs/user-id/as/se/asset-id_thumbnail.webp`; await sut.handleGenerateThumbnails({ id: assetStub.image.id }); @@ -525,8 +583,10 @@ describe(MediaService.name, () => { format, size: 1440, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, previewPath, ); @@ -537,8 +597,10 @@ describe(MediaService.name, () => { format: ImageFormat.Webp, size: 250, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, thumbnailPath, ); @@ -549,8 +611,8 @@ describe(MediaService.name, () => { mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); 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 = 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}`); await sut.handleGenerateThumbnails({ id: assetStub.image.id }); @@ -570,8 +632,10 @@ describe(MediaService.name, () => { format: ImageFormat.Jpeg, size: 1440, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, previewPath, ); @@ -582,20 +646,120 @@ 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 () => { + mocks.systemMetadata.get.mockResolvedValue({ + image: { preview: { progressive: true }, thumbnail: { progressive: false } }, + }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + + await sut.handleGenerateThumbnails({ id: assetStub.image.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, + }), + expect.objectContaining({ + type: AssetFileType.Thumbnail, + isProgressive: false, + }), + ]); + }); + + it('should generate progressive JPEG for thumbnail when enabled', async () => { + mocks.systemMetadata.get.mockResolvedValue({ + image: { preview: { progressive: false }, thumbnail: { format: ImageFormat.Jpeg, progressive: true } }, + }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); + + await sut.handleGenerateThumbnails({ id: assetStub.image.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, + }), + expect.objectContaining({ + type: AssetFileType.Thumbnail, + isProgressive: true, + }), + ]); + }); + + it('should never set isProgressive for videos', async () => { + mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR); + mocks.systemMetadata.get.mockResolvedValue({ + image: { preview: { progressive: true }, thumbnail: { progressive: true } }, + }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); + + await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + + expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ + expect.objectContaining({ + type: AssetFileType.Preview, + isProgressive: false, + }), + expect.objectContaining({ + type: AssetFileType.Thumbnail, + isProgressive: false, + }), + ]); + }); + it('should delete previous thumbnail if different path', async () => { mocks.systemMetadata.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.Webp } } }); mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image); await sut.handleGenerateThumbnails({ id: assetStub.image.id }); - expect(mocks.storage.unlink).toHaveBeenCalledWith('/uploads/user-id/webp/path.ext'); + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { + files: expect.arrayContaining([previewFile.path]), + }, + }); }); it('should extract embedded image if enabled and available', async () => { @@ -641,7 +805,6 @@ describe(MediaService.name, () => { processInvalidImages: false, size: 1440, }); - expect(mocks.media.getImageDimensions).not.toHaveBeenCalled(); }); it('should resize original image if embedded image extraction is not enabled', async () => { @@ -657,7 +820,6 @@ describe(MediaService.name, () => { processInvalidImages: false, size: 1440, }); - expect(mocks.media.getImageDimensions).not.toHaveBeenCalled(); }); it('should process invalid images if enabled', async () => { @@ -691,7 +853,6 @@ describe(MediaService.name, () => { expect.objectContaining({ processInvalidImages: false }), ); - expect(mocks.media.getImageDimensions).not.toHaveBeenCalled(); vi.unstubAllEnvs(); }); @@ -720,8 +881,10 @@ describe(MediaService.name, () => { format: ImageFormat.Jpeg, size: 1440, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -750,8 +913,10 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Webp, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -762,8 +927,10 @@ describe(MediaService.name, () => { format: ImageFormat.Jpeg, size: 1440, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -790,8 +957,10 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Jpeg, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -801,9 +970,11 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Jpeg, quality: 80, + progressive: false, size: 1440, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -831,8 +1002,10 @@ describe(MediaService.name, () => { colorspace: Colorspace.P3, format: ImageFormat.Jpeg, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -886,8 +1059,10 @@ describe(MediaService.name, () => { colorspace: Colorspace.Srgb, format: ImageFormat.Jpeg, quality: 80, + progressive: false, processInvalidImages: false, raw: rawInfo, + edits: [], }, expect.any(String), ); @@ -924,12 +1099,188 @@ 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.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif); + + await sut.handleGenerateThumbnails({ id: assetStub.image.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 + ), + ); + }); + + it('should skip videos', async () => { + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.video); + + await expect(sut.handleAssetEditThumbnailGeneration({ id: assetStub.video.id })).resolves.toBe(JobStatus.Success); + expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); + }); + + it('should upsert 3 edited files for edit jobs', async () => { + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ + ...assetStub.withCropEdit, + }); + const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); + mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); + mocks.person.getFaces.mockResolvedValue([]); + mocks.ocr.getByAssetId.mockResolvedValue([]); + + await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.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 () => { + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ + ...assetStub.withCropEdit, + }); + mocks.person.getFaces.mockResolvedValue([]); + mocks.ocr.getByAssetId.mockResolvedValue([]); + + await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id }); + expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + expect.objectContaining({ + edits: [ + { + 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 () => { + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ + ...assetStub.withoutEdits, + }); + + const status = await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id }); + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { + files: expect.arrayContaining([ + '/uploads/user-id/fullsize/path_edited.jpg', + '/uploads/user-id/preview/path_edited.jpg', + '/uploads/user-id/thumbnail/path_edited.jpg', + ]), + }, + }); + + expect(mocks.asset.deleteFiles).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ path: '/uploads/user-id/preview/path_edited.jpg' }), + expect.objectContaining({ path: '/uploads/user-id/thumbnail/path_edited.jpg' }), + expect.objectContaining({ path: '/uploads/user-id/fullsize/path_edited.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 () => { + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ + ...assetStub.withCropEdit, + }); + mocks.person.getFaces.mockResolvedValue([]); + mocks.ocr.getByAssetId.mockResolvedValue([]); + + await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.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 () => { + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ + ...assetStub.withoutEdits, + }); + const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); + mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); + + await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id, source: 'upload' }); + + expect(mocks.media.generateThumbhash).toHaveBeenCalled(); + }); + + it('should apply thumbhash if job source is edit and edits exist', async () => { + mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue({ + ...assetStub.withCropEdit, + }); + const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); + mocks.media.generateThumbhash.mockResolvedValue(thumbhashBuffer); + mocks.person.getFaces.mockResolvedValue([]); + mocks.ocr.getByAssetId.mockResolvedValue([]); + + await sut.handleAssetEditThumbnailGeneration({ id: assetStub.image.id }); + + expect(mocks.asset.update).toHaveBeenCalledWith( + expect.objectContaining({ + thumbhash: thumbhashBuffer, + }), + ); + }); }); describe('handleGeneratePersonThumbnail', () => { @@ -981,12 +1332,18 @@ 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, @@ -1020,12 +1377,18 @@ 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, @@ -1057,12 +1420,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, @@ -1094,12 +1463,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, @@ -1131,12 +1506,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, @@ -1168,12 +1549,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, @@ -1210,12 +1597,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, @@ -2999,4 +3392,379 @@ 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, + }, + { + assetId: asset.id, + type: AssetFileType.Thumbnail, + path: '/new/thumbnail.jpg', + isEdited: false, + isProgressive: false, + }, + ]); + + expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ + { + assetId: 'asset-id', + path: '/new/preview.jpg', + type: AssetFileType.Preview, + isEdited: false, + isProgressive: false, + }, + { + assetId: 'asset-id', + path: '/new/thumbnail.jpg', + type: AssetFileType.Thumbnail, + isEdited: false, + isProgressive: 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, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: false, + }, + ], + }; + + await sut['syncFiles'](asset.files, [ + { + assetId: asset.id, + type: AssetFileType.Preview, + path: '/new/preview.jpg', + isEdited: false, + isProgressive: false, + }, + { + assetId: asset.id, + type: AssetFileType.Thumbnail, + path: '/new/thumbnail.jpg', + isEdited: false, + isProgressive: false, + }, + ]); + + expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ + { + assetId: 'asset-id', + path: '/new/preview.jpg', + type: AssetFileType.Preview, + isEdited: false, + isProgressive: false, + }, + { + assetId: 'asset-id', + path: '/new/thumbnail.jpg', + type: AssetFileType.Thumbnail, + isEdited: false, + isProgressive: 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, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: 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, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: 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, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/same/thumbnail.jpg', + isEdited: false, + isProgressive: false, + }, + ], + }; + + await sut['syncFiles'](asset.files, [ + { + assetId: asset.id, + type: AssetFileType.Preview, + path: '/same/preview.jpg', + isEdited: false, + isProgressive: false, + }, + { + assetId: asset.id, + type: AssetFileType.Thumbnail, + path: '/same/thumbnail.jpg', + isEdited: false, + isProgressive: 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, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: false, + }, + ], + }; + + await sut['syncFiles'](asset.files, [ + { + assetId: asset.id, + type: AssetFileType.Preview, + path: '/new/preview.jpg', + isEdited: false, + isProgressive: false, + }, // replace + { + assetId: asset.id, + type: AssetFileType.FullSize, + path: '/new/fullsize.jpg', + isEdited: false, + isProgressive: false, + }, // new + ]); + + expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ + { + assetId: 'asset-id', + path: '/new/preview.jpg', + type: AssetFileType.Preview, + isEdited: false, + isProgressive: false, + }, + { + assetId: 'asset-id', + path: '/new/fullsize.jpg', + type: AssetFileType.FullSize, + isEdited: false, + isProgressive: false, + }, + ]); + expect(mocks.asset.deleteFiles).toHaveBeenCalledWith([ + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: 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, + }, + ], + }; + + 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, + }, + ]); + 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, + }, + { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: false, + }, + ], + }; + + await sut['syncFiles'](asset.files, [ + { + assetId: asset.id, + type: AssetFileType.Preview, + path: '/old/preview.jpg', + isEdited: false, + isProgressive: true, + }, + { + assetId: asset.id, + type: AssetFileType.Thumbnail, + path: '/old/thumbnail.jpg', + isEdited: false, + isProgressive: false, + }, + ]); + + expect(mocks.asset.upsertFiles).toHaveBeenCalledWith([ + { + assetId: 'asset-id', + path: '/old/preview.jpg', + type: AssetFileType.Preview, + isEdited: false, + isProgressive: true, + }, + ]); + 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 917df1d8fd..b9b8d74737 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 } 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, @@ -24,12 +25,13 @@ import { 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,16 +39,23 @@ import { VideoInterfaces, VideoStreamInfo, } from 'src/types'; -import { getAssetFiles } from 'src/utils/asset.util'; +import { getAssetFiles, 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; } +type ThumbnailAsset = NonNullable>>; + @Injectable() export class MediaService extends BaseService { videoInterfaces: VideoInterfaces = { dri: [], mali: false }; @@ -67,12 +76,19 @@ export class MediaService extends BaseService { }; for await (const asset of this.assetJobRepository.streamForThumbnailJob(!!force)) { - const { previewFile, thumbnailFile } = getAssetFiles(asset.files); + const assetFiles = getAssetFiles(asset.files); - if (!previewFile || !thumbnailFile || !asset.thumbhash || force) { + if (!assetFiles.previewFile || !assetFiles.thumbnailFile || !asset.thumbhash || force) { jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } }); } + if ( + asset.edits.length > 0 && + (!assetFiles.editedPreviewFile || !assetFiles.editedThumbnailFile || !assetFiles.editedFullsizeFile || force) + ) { + jobs.push({ name: JobName.AssetEditThumbnailGeneration, data: { id: asset.id } }); + } + if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { await queueAll(); } @@ -146,17 +162,58 @@ 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); 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((asset) => asset.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 in database or missing metadata`); return JobStatus.Failed; @@ -167,71 +224,30 @@ export class MediaService extends BaseService { return JobStatus.Skipped; } - let generated: { - previewPath: string; - thumbnailPath: string; - fullsizePath?: string; - 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 } = 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 (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 (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; } @@ -258,27 +274,16 @@ 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( + const { data, info, colorspace } = await this.decodeImage( extracted ? extracted.buffer : asset.originalPath, // only specify orientation to extracted images which don't have EXIF orientation data // or it can double rotate the image @@ -286,33 +291,76 @@ export class MediaService extends BaseService { convertFullsize ? undefined : image.preview.size, ); + return { + extracted, + data, + info, + colorspace, + convertFullsize, + generateFullsize, + }; + } + + private async generateImageThumbnails(asset: ThumbnailAsset, { image }: SystemConfig, useEdits: boolean = false) { + const previewFile = this.getImageFile(asset, { + fileType: AssetFileType.Preview, + format: image.preview.format, + isEdited: useEdits, + isProgressive: !!image.preview.progressive && image.preview.format !== ImageFormat.Webp, + }); + const thumbnailFile = this.getImageFile(asset, { + fileType: AssetFileType.Thumbnail, + format: image.thumbnail.format, + isEdited: useEdits, + isProgressive: !!image.thumbnail.progressive && image.thumbnail.format !== ImageFormat.Webp, + }); + this.storageCore.ensureFolders(previewFile.path); + + // Handle embedded preview extraction for RAW files + const extractedImage = await this.extractOriginalImage(asset, image, useEdits); + const { info, data, colorspace, generateFullsize, convertFullsize, extracted } = extractedImage; + // generate final images - const thumbnailOptions = { colorspace, processInvalidImages: false, raw: info }; + const thumbnailOptions = { colorspace, processInvalidImages: false, raw: info, edits: useEdits ? asset.edits : [] }; 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.generateThumbnail(data, { ...image.thumbnail, ...thumbnailOptions }, thumbnailFile.path), + this.mediaRepository.generateThumbnail(data, { ...image.preview, ...thumbnailOptions }, previewFile.path), ]; - let fullsizePath: string | undefined; - + let fullsizeFile: UpsertFileOptions | undefined; if (convertFullsize) { // 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: image.fullsize.format, + isEdited: useEdits, + isProgressive: !!image.fullsize.progressive && image.fullsize.format !== ImageFormat.Webp, + }); + const fullsizeOptions = { + format: image.fullsize.format, + quality: image.fullsize.quality, + progressive: image.fullsize.progressive, + ...thumbnailOptions, + }; + 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, + }); + 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, ); } @@ -320,15 +368,22 @@ export class MediaService extends BaseService { if (asset.exifInfo.projectionType === 'EQUIRECTANGULAR') { const promises = [ - this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, previewPath), - fullsizePath - ? this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, fullsizePath) + this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, previewFile.path), + fullsizeFile + ? this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, fullsizeFile.path) : Promise.resolve(), ]; await Promise.all(promises); } - return { previewPath, thumbnailPath, fullsizePath, thumbhash: outputs[0] as Buffer }; + const decodedDimensions = { width: info.width, height: info.height }; + const fullsizeDimensions = useEdits ? getOutputDimensions(asset.edits, decodedDimensions) : decodedDimensions; + + return { + files: fullsizeFile ? [previewFile, thumbnailFile, fullsizeFile] : [previewFile, thumbnailFile], + thumbhash: outputs[0] as Buffer, + fullsizeDimensions, + }; } @OnJob({ name: JobName.PersonGenerateThumbnail, queue: QueueName.ThumbnailGeneration }) @@ -369,17 +424,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); @@ -388,7 +449,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); @@ -416,18 +480,30 @@ 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, + }); + const thumbnailFile = this.getImageFile(asset, { + fileType: AssetFileType.Thumbnail, + format: image.thumbnail.format, + isEdited: false, + isProgressive: false, + }); + this.storageCore.ensureFolders(previewFile.path); const { format, audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath); const mainVideoStream = this.getMainStream(videoStreams); @@ -446,15 +522,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 }) @@ -707,4 +787,87 @@ export class MediaService extends BaseService { return false; } } + + private async syncFiles(oldFiles: (AssetFile & { isProgressive: 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) { + 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 getImageFile(asset: ThumbnailPathEntity, options: ImagePathOptions & { isProgressive: boolean }) { + const path = StorageCore.getImagePath(asset, options); + return { + assetId: asset.id, + type: options.fileType, + path, + isEdited: options.isEdited, + isProgressive: options.isProgressive, + }; + } } diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 98c906d9c7..942817a213 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -35,7 +35,7 @@ const forSidecarJob = ( asset: { id?: string; originalPath?: string; - files?: { id: string; type: AssetFileType; path: string }[]; + files?: { id: string; type: AssetFileType; path: string; isEdited: boolean }[]; } = {}, ) => { return { @@ -224,6 +224,8 @@ describe(MetadataService.name, () => { fileCreatedAt: fileModifiedAt, fileModifiedAt, localDateTime: fileModifiedAt, + width: null, + height: null, }); }); @@ -251,6 +253,8 @@ describe(MetadataService.name, () => { fileCreatedAt, fileModifiedAt, localDateTime: fileCreatedAt, + width: null, + height: null, }); }); @@ -297,6 +301,8 @@ describe(MetadataService.name, () => { fileCreatedAt: assetStub.image.fileCreatedAt, fileModifiedAt: assetStub.image.fileCreatedAt, localDateTime: assetStub.image.fileCreatedAt, + width: null, + height: null, }); }); @@ -327,6 +333,8 @@ describe(MetadataService.name, () => { fileCreatedAt: assetStub.withLocation.fileCreatedAt, fileModifiedAt: assetStub.withLocation.fileModifiedAt, localDateTime: new Date('2023-02-22T05:06:29.716Z'), + width: null, + height: null, }); }); @@ -357,6 +365,8 @@ describe(MetadataService.name, () => { fileCreatedAt: assetStub.withLocation.fileCreatedAt, fileModifiedAt: assetStub.withLocation.fileModifiedAt, localDateTime: new Date('2023-02-22T05:06:29.716Z'), + width: null, + height: null, }); }); @@ -377,6 +387,7 @@ describe(MetadataService.name, () => { it('should extract tags from TagsList', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent'] }) }); mockReadTags({ TagsList: ['Parent'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -387,6 +398,7 @@ describe(MetadataService.name, () => { it('should extract hierarchy from TagsList', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent/Child'] }) }); mockReadTags({ TagsList: ['Parent/Child'] }); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert); @@ -407,6 +419,7 @@ describe(MetadataService.name, () => { it('should extract tags from Keywords as a string', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent'] }) }); mockReadTags({ Keywords: 'Parent' }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -417,6 +430,7 @@ describe(MetadataService.name, () => { it('should extract tags from Keywords as a list', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent'] }) }); mockReadTags({ Keywords: ['Parent'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -427,6 +441,10 @@ describe(MetadataService.name, () => { it('should extract tags from Keywords as a list with a number', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ + ...factory.asset(), + exifInfo: factory.exif({ tags: ['Parent', '2024'] }), + }); mockReadTags({ Keywords: ['Parent', 2024] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -438,6 +456,7 @@ describe(MetadataService.name, () => { it('should extract hierarchal tags from Keywords', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Parent/Child'] }) }); mockReadTags({ Keywords: 'Parent/Child' }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -457,6 +476,10 @@ describe(MetadataService.name, () => { it('should ignore Keywords when TagsList is present', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ + ...factory.asset(), + exifInfo: factory.exif({ tags: ['Parent/Child', 'Child'] }), + }); mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -476,6 +499,10 @@ describe(MetadataService.name, () => { it('should extract hierarchy from HierarchicalSubject', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ + ...factory.asset(), + exifInfo: factory.exif({ tags: ['Parent/Child', 'TagA'] }), + }); mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] }); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert); @@ -497,6 +524,10 @@ describe(MetadataService.name, () => { it('should extract tags from HierarchicalSubject as a list with a number', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); + mocks.asset.getById.mockResolvedValue({ + ...factory.asset(), + exifInfo: factory.exif({ tags: ['Parent', '2024'] }), + }); mockReadTags({ HierarchicalSubject: ['Parent', 2024] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -508,6 +539,7 @@ describe(MetadataService.name, () => { it('should extract ignore / characters in a HierarchicalSubject tag', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.asset.getById.mockResolvedValue({ ...factory.asset(), exifInfo: factory.exif({ tags: ['Mom|Dad'] }) }); mockReadTags({ HierarchicalSubject: ['Mom/Dad'] }); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); @@ -522,6 +554,10 @@ describe(MetadataService.name, () => { it('should ignore HierarchicalSubject when TagsList is present', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.asset.getById.mockResolvedValue({ + ...factory.asset(), + exifInfo: factory.exif({ tags: ['Parent/Child', 'Parent2/Child2'] }), + }); mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -886,6 +922,7 @@ describe(MetadataService.name, () => { ProfileDescription: 'extensive description', ProjectionType: 'equirectangular', tz: 'UTC-11:30', + TagsList: ['parent/child'], Rating: 3, }; @@ -925,6 +962,7 @@ describe(MetadataService.name, () => { country: null, state: null, city: null, + tags: ['parent/child'], }, { lockedPropertiesBehavior: 'skip' }, ); @@ -1074,6 +1112,7 @@ describe(MetadataService.name, () => { id: 'some-id', type: AssetFileType.Sidecar, path: '/path/to/something', + isEdited: false, }, ], }); @@ -1560,6 +1599,49 @@ describe(MetadataService.name, () => { { lockedPropertiesBehavior: 'skip' }, ); }); + + it('should properly set width/height for normal images', async () => { + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mockReadTags({ ImageWidth: 1000, ImageHeight: 2000 }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(mocks.asset.update).toHaveBeenCalledWith( + expect.objectContaining({ + width: 1000, + height: 2000, + }), + ); + }); + + it('should properly swap asset width/height for rotated images', async () => { + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mockReadTags({ ImageWidth: 1000, ImageHeight: 2000, Orientation: 6 }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(mocks.asset.update).toHaveBeenCalledWith( + expect.objectContaining({ + width: 2000, + height: 1000, + }), + ); + }); + + it('should not overwrite existing width/height if they already exist', async () => { + mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ + ...assetStub.image, + width: 1920, + height: 1080, + }); + mockReadTags({ ImageWidth: 1280, ImageHeight: 720 }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(mocks.asset.update).not.toHaveBeenCalledWith( + expect.objectContaining({ + width: 1280, + height: 720, + }), + ); + }); }); describe('handleQueueSidecar', () => { @@ -1638,7 +1720,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 +1733,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); @@ -1705,6 +1787,12 @@ describe(MetadataService.name, () => { GPSLatitude: gps, GPSLongitude: gps, }); + expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, [ + 'description', + 'latitude', + 'longitude', + 'dateTimeOriginal', + ]); }); }); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 3e5b220c04..f74f9f4cec 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -196,6 +196,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 +254,8 @@ export class MetadataService extends BaseService { } } + const tags = this.getTagList(exifTags); + const exifData: Insertable = { assetId: asset.id, @@ -287,8 +298,14 @@ export class MetadataService extends BaseService { // grouping livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null, autoStackId: this.getAutoStackId(exifTags), + + tags: tags.length > 0 ? tags : null, }; + const isSidewards = exifTags.Orientation && this.isOrientationSidewards(exifTags.Orientation); + const assetWidth = isSidewards ? validate(height) : validate(width); + const assetHeight = isSidewards ? validate(width) : validate(height); + const promises: Promise[] = [ this.assetRepository.upsertExif(exifData, { lockedPropertiesBehavior: 'skip' }), this.assetRepository.update({ @@ -297,10 +314,16 @@ export class MetadataService extends BaseService { 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, }), - this.applyTagList(asset, exifTags), ]; + await this.applyTagList(asset); + if (this.isMotionPhoto(asset, exifTags)) { promises.push(this.applyMotionPhotos(asset, exifTags, dates, stats)); } @@ -387,35 +410,35 @@ 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 } = _.pick( { description: asset.exifInfo.description, dateTimeOriginal: asset.exifInfo.dateTimeOriginal, latitude: asset.exifInfo.latitude, longitude: asset.exifInfo.longitude, rating: asset.exifInfo.rating, + tags: asset.exifInfo.tags, }, lockedProperties, ); @@ -428,7 +451,7 @@ export class MetadataService extends BaseService { GPSLatitude: latitude, GPSLongitude: longitude, Rating: rating, - TagsList: tags ? tagsList : undefined, + TagsList: tags?.length ? tags : undefined, }, _.isUndefined, ); @@ -443,6 +466,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; } @@ -540,11 +565,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.getById(id, { exifInfo: true }); + const results = await upsertTags(this.tagRepository, { + userId: ownerId, + tags: asset?.exifInfo?.tags ?? [], + }); await this.tagRepository.replaceAssetTags( - asset.id, + id, results.map((tag) => tag.id), ); } @@ -716,12 +744,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 @@ -971,9 +994,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.service.spec.ts b/server/src/services/notification.service.spec.ts index daa3f221ae..6627ffea8a 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -372,7 +372,7 @@ describe(NotificationService.name, () => { 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' }, + { id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg', isEdited: false }, ]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); @@ -403,7 +403,7 @@ 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([{ ...assetStub.image.files[2], isEdited: false }]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 41c44ea476..b57a5e1072 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -354,6 +354,7 @@ describe(PersonService.name, () => { 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]); + mocks.asset.getById.mockResolvedValue(assetStub.image); await expect(sut.getFacesById(authStub.admin, { id: faceStub.face1.assetId })).resolves.toStrictEqual([ mapFaces(faceStub.primaryFace1, authStub.admin), ]); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 6fa9b3fdd2..dfbb56bd1e 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -40,6 +40,7 @@ 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'; @@ -126,7 +127,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.getById(dto.id, { edits: true, exifInfo: true }); + const assetDimensions = getDimensions(asset!.exifInfo!); + + return faces.map((face) => mapFaces(face, auth, asset!.edits!, assetDimensions)); } async createNewFeaturePhoto(changeFeaturePhoto: string[]) { 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/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/storage-template.service.ts b/server/src/services/storage-template.service.ts index cd641d7036..d5020a9c5e 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -240,11 +240,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`, }); 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..f42f40940d 100644 --- a/server/src/services/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -4,6 +4,7 @@ import { JobStatus } from 'src/enum'; import { TagService } from 'src/services/tag.service'; import { authStub } from 'test/fixtures/auth.stub'; import { tagResponseStub, tagStub } from 'test/fixtures/tag.stub'; +import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; describe(TagService.name, () => { @@ -191,6 +192,10 @@ 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.getById.mockResolvedValue({ + ...factory.asset(), + tags: [factory.tag({ value: 'tag-1' }), factory.tag({ value: 'tag-2' })], + }); mocks.tag.upsertAssetIds.mockResolvedValue([ { tagId: 'tag-1', assetId: 'asset-1' }, { tagId: 'tag-1', assetId: 'asset-2' }, @@ -204,6 +209,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 +246,10 @@ describe(TagService.name, () => { mocks.tag.get.mockResolvedValue(tagStub.tag); mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1'])); mocks.tag.addAssetIds.mockResolvedValue(); + mocks.asset.getById.mockResolvedValue({ + ...factory.asset(), + tags: [factory.tag({ value: 'tag-1' })], + }); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-2'])); await expect( @@ -240,6 +261,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']); }); diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index 3ee5d29b75..20303421c1 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,12 @@ export class TagService extends BaseService { } return tag; } + + private async updateTags(assetId: string) { + const asset = await this.assetRepository.getById(assetId, { tags: true }); + await this.assetRepository.upsertExif( + updateLockedColumns({ assetId, tags: asset?.tags?.map(({ value }) => value) ?? [] }), + { lockedPropertiesBehavior: 'append' }, + ); + } } 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/types.ts b/server/src/types.ts index 779de1ee37..3e9ea25957 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -3,6 +3,8 @@ 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, @@ -21,48 +23,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 +74,6 @@ export type GenerateThumbhashFromBufferOptions = GenerateThumbhashOptions & { ra export interface GenerateThumbnailsOptions { colorspace: string; - crop?: CropOptions; preview?: ImageOptions; processInvalidImages: boolean; thumbhash?: boolean; @@ -186,7 +187,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 +325,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,7 +386,10 @@ 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]; @@ -485,7 +489,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; @@ -505,7 +511,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 f3f807c829..f8fb3d215d 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -1,9 +1,10 @@ import { BadRequestException } from '@nestjs/common'; -import { GeneratedImageType, StorageCore } from 'src/cores/storage.core'; -import { AssetFile } from 'src/database'; +import { StorageCore } from 'src/cores/storage.core'; +import { AssetFile, Exif } from 'src/database'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { ExifResponseDto } from 'src/dtos/exif.dto'; import { AssetFileType, AssetType, AssetVisibility, Permission } from 'src/enum'; import { AuthRequest } from 'src/middleware/auth.guard'; import { AccessRepository } from 'src/repositories/access.repository'; @@ -13,15 +14,19 @@ 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), + fullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: false }), + previewFile: getAssetFile(files, AssetFileType.Preview, { isEdited: false }), + thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail, { 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 ( @@ -199,3 +204,26 @@ 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 = (exifInfo: ExifResponseDto | Exif) => { + const { exifImageWidth: width, exifImageHeight: height } = exifInfo; + + if (!width || !height) { + return { width: 0, height: 0 }; + } + + if (isFlipped(exifInfo.orientation)) { + return { width: height, height: width }; + } + + return { width, height }; +}; + +export const isPanorama = (asset: { exifInfo?: Exif | null; originalFileName: string }) => { + return asset.exifInfo?.projectionType === 'EQUIRECTANGULAR' || asset.originalFileName.toLowerCase().endsWith('.insp'); +}; diff --git a/server/src/utils/database-backups.ts b/server/src/utils/database-backups.ts new file mode 100644 index 0000000000..83d52fc531 --- /dev/null +++ b/server/src/utils/database-backups.ts @@ -0,0 +1,494 @@ +import { BadRequestException } from '@nestjs/common'; +import { debounce } from 'lodash'; +import { DateTime } from 'luxon'; +import path, { basename, join } 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 { CacheControl, StorageFolder } from 'src/enum'; +import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.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'; + +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 findVersion(filename: string) { + return /-v(.*)-/.exec(filename)?.[1]; +} + +type BackupRepos = { + logger: LoggingRepository; + storage: StorageRepository; + config: ConfigRepository; + process: ProcessRepository; + database: DatabaseRepository; + health: MaintenanceHealthRepository; +}; + +export class UnsupportedPostgresError extends Error { + constructor(databaseVersion: string) { + super(`Unsupported PostgreSQL version: ${databaseVersion}`); + } +} + +export async function buildPostgresLaunchArguments( + { logger, config, database }: Pick, + bin: 'pg_dump' | 'pg_dumpall' | 'psql', + options: { + singleTransaction?: boolean; + username?: string; + } = {}, +): Promise<{ + bin: string; + args: string[]; + databasePassword: string; + databaseVersion: string; + databaseMajorVersion?: number; +}> { + const { + database: { config: databaseConfig }, + } = config.getEnv(); + const isUrlConnection = databaseConfig.connectionType === 'url'; + + const databaseVersion = await database.getPostgresVersion(); + const databaseSemver = semver.coerce(databaseVersion); + const databaseMajorVersion = databaseSemver?.major; + + const args: string[] = []; + + 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'); + + if (options.username) { + parsedUrl.username = options.username; + } + + url = parsedUrl.toString(); + } + + args.push(url); + } else { + args.push( + '--username', + options.username ?? databaseConfig.username, + '--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')) { + logger.error(`Database Restore Failure: Unsupported PostgreSQL version: ${databaseVersion}`); + throw new UnsupportedPostgresError(databaseVersion); + } + + return { + bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`, + args, + databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password, + databaseVersion, + databaseMajorVersion, + }; +} + +export async function createDatabaseBackup( + { logger, storage, process: processRepository, ...pgRepos }: Omit, + filenamePrefix: string = '', +): Promise { + logger.debug(`Database Backup Started`); + + const { bin, args, databasePassword, databaseVersion, databaseMajorVersion } = await buildPostgresLaunchArguments( + { logger, ...pgRepos }, + 'pg_dump', + ); + + 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 = join(StorageCore.getBaseFolder(StorageFolder.Backups), filename); + const temporaryFilePath = `${backupFilePath}.tmp`; + + try { + const pgdump = processRepository.spawnDuplexStream(bin, args, { + env: { + PATH: process.env.PATH, + PGPASSWORD: databasePassword, + }, + }); + + const gzip = processRepository.spawnDuplexStream('gzip', ['--rsyncable']); + const fileStream = storage.createWriteStream(temporaryFilePath); + + await pipeline(pgdump, gzip, fileStream); + await storage.rename(temporaryFilePath, backupFilePath); + } catch (error) { + logger.error(`Database Backup Failure: ${error}`); + await storage + .unlink(temporaryFilePath) + .catch((error) => logger.error(`Failed to delete failed backup file: ${error}`)); + throw error; + } + + logger.log(`Database Backup Success`); + return backupFilePath; +} + +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 = ` + -- re-create the default schema + DROP SCHEMA public CASCADE; + CREATE SCHEMA public; + + -- restore access to schema + GRANT ALL ON SCHEMA public TO postgres; + GRANT ALL ON SCHEMA public TO public; +`; + +async function* sql(inputStream: Readable, isPgClusterDump: boolean) { + yield SQL_DROP_CONNECTIONS; + yield isPgClusterDump + ? String.raw` + \c postgres + ` + : SQL_RESET_SCHEMA; + + for await (const chunk of inputStream) { + yield chunk; + } +} + +async function* sqlRollback(inputStream: Readable, isPgClusterDump: boolean) { + yield SQL_DROP_CONNECTIONS; + + if (isPgClusterDump) { + yield String.raw` + -- try to create database + -- may fail but script will continue running + CREATE DATABASE immich; + + -- switch to database / newly created database + \c immich + `; + } + + yield SQL_RESET_SCHEMA; + + for await (const chunk of inputStream) { + yield chunk; + } +} + +export async function restoreDatabaseBackup( + { logger, storage, process: processRepository, database: databaseRepository, health, ...pgRepos }: BackupRepos, + filename: string, + progressCb?: (action: 'backup' | 'restore' | 'migrations' | 'rollback', progress: number) => void, +): Promise { + 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 storage.stat(backupFilePath); // => check file exists + + let isPgClusterDump = false; + const version = findVersion(filename); + if (version && semver.satisfies(version, '<= 2.4')) { + isPgClusterDump = true; + } + + const { bin, args, databasePassword, databaseMajorVersion } = await buildPostgresLaunchArguments( + { logger, database: databaseRepository, ...pgRepos }, + 'psql', + { + singleTransaction: !isPgClusterDump, + username: isPgClusterDump ? 'postgres' : undefined, + }, + ); + + progressCb?.('backup', 0.05); + + const restorePointFilePath = await createDatabaseBackup( + { logger, storage, process: processRepository, database: databaseRepository, ...pgRepos }, + 'restore-point-', + ); + + logger.log(`Database Restore Starting. Database Version: ${databaseMajorVersion}`); + + let inputStream: Readable; + if (backupFilePath.endsWith('.gz')) { + const fileStream = storage.createPlainReadStream(backupFilePath); + const gunzip = storage.createGunzip(); + fileStream.pipe(gunzip); + inputStream = gunzip; + } else { + inputStream = storage.createPlainReadStream(backupFilePath); + } + + const sqlStream = Readable.from(sql(inputStream, isPgClusterDump)); + const psql = processRepository.spawnDuplexStream(bin, args, { + env: { + PATH: process.env.PATH, + PGPASSWORD: databasePassword, + }, + }); + + const [progressSource, progressSink] = createSqlProgressStreams((progress) => { + if (complete) { + return; + } + + logger.log(`Restore progress ~ ${(progress * 100).toFixed(2)}%`); + progressCb?.('restore', progress); + }); + + await pipeline(sqlStream, progressSource, psql, progressSink); + + try { + progressCb?.('migrations', 0.9); + await databaseRepository.runMigrations(); + await health.checkApiHealth(); + } catch (error) { + progressCb?.('rollback', 0); + + const fileStream = storage.createPlainReadStream(restorePointFilePath); + const gunzip = storage.createGunzip(); + fileStream.pipe(gunzip); + inputStream = gunzip; + + const sqlStream = Readable.from(sqlRollback(inputStream, isPgClusterDump)); + const psql = processRepository.spawnDuplexStream(bin, args, { + env: { + PATH: process.env.PATH, + PGPASSWORD: databasePassword, + }, + }); + + const [progressSource, progressSink] = createSqlProgressStreams((progress) => { + if (complete) { + return; + } + + logger.log(`Rollback progress ~ ${(progress * 100).toFixed(2)}%`); + progressCb?.('rollback', progress); + }); + + await pipeline(sqlStream, progressSource, psql, progressSink); + + throw error; + } + } catch (error) { + logger.error(`Database Restore Failure: ${error}`); + throw error; + } finally { + complete = true; + } + + logger.log(`Database Restore Success`); +} + +export async function deleteDatabaseBackup({ storage }: Pick, 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) => storage.unlink(path.join(backupsFolder, filename)))); +} + +export async function listDatabaseBackups({ + storage, +}: Pick): Promise<{ filename: string; filesize: number }[]> { + const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups); + const files = await storage.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 storage.stat(path.join(backupsFolder, filename)); + return { filename, filesize: stats.size }; + }), + ); + + return backups; +} + +export async function uploadDatabaseBackup( + { storage }: Pick, + 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 path = join(backupsFolder, `uploaded-${fn}`); + await storage.createOrOverwriteFile(path, file.buffer); +} + +export function downloadDatabaseBackup(fileName: string) { + if (!isValidDatabaseBackupName(fileName)) { + throw new BadRequestException('Invalid backup name!'); + } + + const path = join(StorageCore.getBaseFolder(StorageFolder.Backups), fileName); + + return { + path, + fileName, + cacheControl: CacheControl.PrivateWithoutCache, + contentType: fileName.endsWith('.gz') ? 'application/gzip' : 'application/sql', + }; +} + +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/utils/database.ts b/server/src/utils/database.ts index 95998eb44b..a041946a28 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -1,4 +1,5 @@ import { + AliasedRawBuilder, DeduplicateJoinsPlugin, Expression, ExpressionBuilder, @@ -16,6 +17,7 @@ import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres'; import { parse } from 'pg-connection-string'; import postgres, { Notice, PostgresError } from 'postgres'; import { columns, Exif, lockableProperties, LockableProperty, Person } from 'src/database'; +import { AssetEditActionItem } from 'src/dtos/editing.dto'; import { AssetFileType, AssetVisibility, DatabaseExtension, DatabaseSslMode } from 'src/enum'; import { AssetSearchBuilderOptions } from 'src/repositories/search.repository'; import { DB } from 'src/schema'; @@ -180,13 +182,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 +211,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 +227,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 +240,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 +355,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 */ 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 faa92395d6..47abb0ab89 100644 --- a/server/src/utils/maintenance.ts +++ b/server/src/utils/maintenance.ts @@ -1,6 +1,59 @@ +import { createAdapter } from '@socket.io/redis-adapter'; +import Redis from 'ioredis'; import { SignJWT } from 'jose'; import { randomBytes } from 'node:crypto'; -import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; +import { join } from 'node:path'; +import { Server as SocketIO } from 'socket.io'; +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(); + const { redis } = new ConfigRepository().getEnv(); + const pubClient = new Redis(redis); + const subClient = pubClient.duplicate(); + server.adapter(createAdapter(pubClient, subClient)); + + /** + * Keep trying until we manage to stop Immich + * + * Sometimes there appear to be communication + * issues between to the other servers. + * + * This issue only occurs with this method. + */ + async function tryTerminate() { + while (true) { + try { + const responses = await server.serverSideEmitWithAck('AppRestart', state); + if (responses.length > 0) { + return; + } + } catch (error) { + console.error(error); + console.error('Encountered an error while telling Immich to stop.'); + } + + console.info( + "\nIt doesn't appear that Immich stopped, trying again in a moment.\nIf Immich is already not running, you can ignore this error.", + ); + + await new Promise((r) => setTimeout(r, 1e3)); + } + } + + // => corresponds to notification.service.ts#onAppRestart + server.emit('AppRestartV1', state, () => { + void tryTerminate().finally(() => { + pubClient.disconnect(); + subClient.disconnect(); + }); + }); +} export async function createMaintenanceLoginUrl( baseUrl: string, @@ -23,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/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/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..b57a198cc6 --- /dev/null +++ b/server/src/utils/transform.ts @@ -0,0 +1,227 @@ +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(); + } + } + }), + ); +}; + +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. + */ +const transformPoints = ( + points: Point[], + edits: AssetEditActionItem[], + startingDimensions: ImageDimensions, +): TransformState => { + let currentWidth = startingDimensions.width; + let currentHeight = startingDimensions.height; + let transformedPoints = [...points]; + + // Handle crop first + 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 + for (const edit of edits) { + 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(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)); + } + + 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..724c01ffe9 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 } = { @@ -224,10 +278,10 @@ export const ValidateString = (options?: StringOptions & ApiPropertyOptions) => }; type BooleanOptions = { optional?: boolean; nullable?: boolean }; -export const ValidateBoolean = (options?: BooleanOptions & ApiPropertyOptions) => { +export const ValidateBoolean = (options?: BooleanOptions & PropertyOptions) => { const { optional, nullable, ...apiPropertyOptions } = options || {}; const decorators = [ - ApiProperty(apiPropertyOptions), + Property(apiPropertyOptions), IsBoolean(), Transform(({ value }) => { if (value == 'true') { 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/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 6e4193c110..05219c92e7 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -1,42 +1,63 @@ import { AssetFace, AssetFile, Exif } from 'src/database'; import { MapAsset } from 'src/dtos/asset-response.dto'; +import { AssetEditAction, AssetEditActionItem } from 'src/dtos/editing.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'; +import { factory } from 'test/small.factory'; -export const previewFile: AssetFile = { - id: 'file-1', - type: AssetFileType.Preview, - path: '/uploads/user-id/thumbs/path.jpg', -}; +export const previewFile = factory.assetFile({ type: AssetFileType.Preview }); -const thumbnailFile: AssetFile = { - id: 'file-2', +const thumbnailFile = factory.assetFile({ type: AssetFileType.Thumbnail, path: '/uploads/user-id/webp/path.ext', -}; +}); -const fullsizeFile: AssetFile = { - id: 'file-3', +const fullsizeFile = factory.assetFile({ type: AssetFileType.FullSize, path: '/uploads/user-id/fullsize/path.webp', -}; +}); -const sidecarFileWithExt: AssetFile = { - id: 'sidecar-with-ext', +const sidecarFileWithExt = factory.assetFile({ type: AssetFileType.Sidecar, path: '/original/path.ext.xmp', -}; +}); -const sidecarFileWithoutExt: AssetFile = { - id: 'sidecar-without-ext', +const sidecarFileWithoutExt = factory.assetFile({ type: AssetFileType.Sidecar, path: '/original/path.xmp', -}; +}); -const files: AssetFile[] = [fullsizeFile, previewFile, thumbnailFile]; +const editedPreviewFile = factory.assetFile({ + type: AssetFileType.Preview, + path: '/uploads/user-id/preview/path_edited.jpg', + isEdited: true, +}); + +const editedThumbnailFile = factory.assetFile({ + type: AssetFileType.Thumbnail, + path: '/uploads/user-id/thumbnail/path_edited.jpg', + isEdited: true, +}); + +const editedFullsizeFile = factory.assetFile({ + type: AssetFileType.FullSize, + path: '/uploads/user-id/fullsize/path_edited.jpg', + isEdited: true, +}); + +const files = [fullsizeFile, previewFile, thumbnailFile]; + +const editedFiles = [ + fullsizeFile, + previewFile, + thumbnailFile, + editedFullsizeFile, + editedPreviewFile, + editedThumbnailFile, +]; export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif })[]) => { return { @@ -68,6 +89,7 @@ export const assetStub = { make: 'FUJIFILM', model: 'X-T50', lensModel: 'XF27mm F2.8 R WR', + isEdited: false, ...asset, }), noResizePath: Object.freeze({ @@ -104,6 +126,10 @@ export const assetStub = { stackId: null, updateId: '42', visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), noWebpPath: Object.freeze({ @@ -142,6 +168,10 @@ export const assetStub = { stackId: null, updateId: '42', visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), noThumbhash: Object.freeze({ @@ -177,6 +207,10 @@ export const assetStub = { stackId: null, updateId: '42', visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), primaryImage: Object.freeze({ @@ -222,6 +256,10 @@ export const assetStub = { updateId: '42', libraryId: null, visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), image: Object.freeze({ @@ -264,9 +302,11 @@ export const assetStub = { stack: null, orientation: '', projectionType: null, - height: 3840, - width: 2160, + height: null, + width: null, visibility: AssetVisibility.Timeline, + edits: [], + isEdited: false, }), trashed: Object.freeze({ @@ -307,6 +347,10 @@ export const assetStub = { stackId: null, updateId: '42', visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), trashedOffline: Object.freeze({ @@ -347,6 +391,10 @@ export const assetStub = { stackId: null, updateId: '42', visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), archived: Object.freeze({ id: 'asset-id', @@ -386,6 +434,10 @@ export const assetStub = { stackId: null, updateId: '42', visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), external: Object.freeze({ @@ -425,6 +477,10 @@ export const assetStub = { stackId: null, stack: null, visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), image1: Object.freeze({ @@ -464,6 +520,10 @@ export const assetStub = { libraryId: null, stack: null, visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), imageFrom2015: Object.freeze({ @@ -502,6 +562,10 @@ export const assetStub = { duplicateId: null, isOffline: false, visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), video: Object.freeze({ @@ -542,6 +606,10 @@ export const assetStub = { libraryId: null, stackId: null, visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), livePhotoMotionAsset: Object.freeze({ @@ -556,10 +624,19 @@ export const assetStub = { fileSizeInByte: 100_000, timeZone: `America/New_York`, }, - files: [] as AssetFile[], + files: [], libraryId: null, visibility: AssetVisibility.Hidden, - } as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif }), + width: null, + height: null, + edits: [] as AssetEditActionItem[], + isEdited: false, + } as unknown as MapAsset & { + faces: AssetFace[]; + files: (AssetFile & { isProgressive: boolean })[]; + exifInfo: Exif; + edits: AssetEditActionItem[]; + }), livePhotoStillAsset: Object.freeze({ id: 'live-photo-still-asset', @@ -577,7 +654,15 @@ export const assetStub = { files, faces: [] as AssetFace[], visibility: AssetVisibility.Timeline, - } as MapAsset & { faces: AssetFace[]; files: AssetFile[] }), + width: null, + height: null, + edits: [] as AssetEditActionItem[], + isEdited: false, + } as unknown as MapAsset & { + faces: AssetFace[]; + files: (AssetFile & { isProgressive: boolean })[]; + edits: AssetEditActionItem[]; + }), livePhotoWithOriginalFileName: Object.freeze({ id: 'live-photo-still-asset', @@ -597,7 +682,11 @@ export const assetStub = { libraryId: null, faces: [] as AssetFace[], visibility: AssetVisibility.Timeline, - } as MapAsset & { faces: AssetFace[]; files: AssetFile[] }), + width: null, + height: null, + edits: [] as AssetEditActionItem[], + isEdited: false, + } as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }), withLocation: Object.freeze({ id: 'asset-with-favorite-id', @@ -641,6 +730,10 @@ export const assetStub = { isOffline: false, tags: [], visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), sidecar: Object.freeze({ @@ -676,6 +769,10 @@ export const assetStub = { libraryId: null, stackId: null, visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), sidecarWithoutExt: Object.freeze({ @@ -708,6 +805,10 @@ export const assetStub = { duplicateId: null, isOffline: false, visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), hasEncodedVideo: Object.freeze({ @@ -747,6 +848,10 @@ export const assetStub = { stackId: null, stack: null, visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), hasFileExtension: Object.freeze({ @@ -783,6 +888,10 @@ export const assetStub = { duplicateId: null, isOffline: false, visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), imageDng: Object.freeze({ @@ -823,6 +932,10 @@ export const assetStub = { libraryId: null, stackId: null, visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), imageHif: Object.freeze({ @@ -863,7 +976,12 @@ export const assetStub = { libraryId: null, stackId: null, visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + isEdited: false, }), + panoramaTif: Object.freeze({ id: 'asset-id', status: AssetStatus.Active, @@ -902,5 +1020,114 @@ export const assetStub = { libraryId: null, stackId: null, visibility: AssetVisibility.Timeline, + width: null, + height: null, + edits: [], + }), + + withCropEdit: 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, + sidecarPath: 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, + edits: [ + { + action: AssetEditAction.Crop, + parameters: { + width: 1512, + height: 1152, + x: 216, + y: 1512, + }, + }, + ] as AssetEditActionItem[], + isEdited: true, + }), + + withoutEdits: 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: editedFiles, + 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, + sidecarPath: 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, + edits: [], + isEdited: false, }), }; diff --git a/server/test/fixtures/face.stub.ts b/server/test/fixtures/face.stub.ts index f655a3944e..94a2dcff22 100644 --- a/server/test/fixtures/face.stub.ts +++ b/server/test/fixtures/face.stub.ts @@ -25,6 +25,7 @@ export const faceStub = { deletedAt: new Date(), updatedAt: new Date('2023-01-01T00:00:00Z'), updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', + isVisible: true, }), primaryFace1: Object.freeze({ id: 'assetFaceId2', @@ -43,6 +44,7 @@ export const faceStub = { deletedAt: null, updatedAt: new Date('2023-01-01T00:00:00Z'), updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', + isVisible: true, }), mergeFace1: Object.freeze({ id: 'assetFaceId3', @@ -61,6 +63,7 @@ export const faceStub = { deletedAt: null, updatedAt: new Date('2023-01-01T00:00:00Z'), updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', + isVisible: true, }), noPerson1: Object.freeze({ id: 'assetFaceId8', @@ -79,6 +82,7 @@ export const faceStub = { deletedAt: null, updatedAt: new Date('2023-01-01T00:00:00Z'), updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', + isVisible: true, }), noPerson2: Object.freeze({ id: 'assetFaceId9', @@ -97,6 +101,7 @@ export const faceStub = { deletedAt: null, updatedAt: new Date('2023-01-01T00:00:00Z'), updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', + isVisible: true, }), fromExif1: Object.freeze({ id: 'assetFaceId9', @@ -114,6 +119,7 @@ export const faceStub = { deletedAt: null, updatedAt: new Date('2023-01-01T00:00:00Z'), updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', + isVisible: true, }), fromExif2: Object.freeze({ id: 'assetFaceId9', @@ -131,6 +137,7 @@ export const faceStub = { deletedAt: null, updatedAt: new Date('2023-01-01T00:00:00Z'), updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', + isVisible: true, }), withBirthDate: Object.freeze({ id: 'assetFaceId10', @@ -148,5 +155,6 @@ export const faceStub = { deletedAt: null, updatedAt: new Date('2023-01-01T00:00:00Z'), updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125', + isVisible: true, }), }; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 802b46a986..859b6b6ae2 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -142,6 +142,12 @@ export const sharedLinkStub = { rating: 3, updatedAt: today, updateId: '42', + libraryId: null, + stackId: null, + visibility: AssetVisibility.Timeline, + width: 500, + height: 500, + tags: [], }, sharedLinks: [], faces: [], @@ -152,6 +158,9 @@ export const sharedLinkStub = { libraryId: null, stackId: null, visibility: AssetVisibility.Timeline, + width: 500, + height: 500, + isEdited: false, }, ], albumId: null, diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 44ca231d8f..153b568222 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -19,6 +19,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'; @@ -384,6 +385,7 @@ const newRealRepository = (key: ClassConstructor, db: Kysely): T => { case AlbumUserRepository: case ActivityRepository: case AssetRepository: + case AssetEditRepository: case AssetJobRepository: case MemoryRepository: case NotificationRepository: @@ -535,6 +537,7 @@ const assetInsert = (asset: Partial> = {}) => { fileModifiedAt: now, localDateTime: now, visibility: AssetVisibility.Timeline, + isEdited: false, }; return { @@ -581,6 +584,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 { @@ -597,8 +601,6 @@ const assetJobStatusInsert = ( duplicatesDetectedAt: date, facesRecognizedAt: date, metadataExtractedAt: date, - previewAt: date, - thumbnailAt: date, }; return { 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.repository.spec.ts b/server/test/medium/specs/repositories/asset.repository.spec.ts index a7af66f872..97f503e9ed 100644 --- a/server/test/medium/specs/repositories/asset.repository.spec.ts +++ b/server/test/medium/specs/repositories/asset.repository.spec.ts @@ -87,4 +87,64 @@ describe(AssetRepository.name, () => { ).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/services/asset-media.service.spec.ts b/server/test/medium/specs/services/asset-media.service.spec.ts index 5089850b6f..cdd47e3dc4 100644 --- a/server/test/medium/specs/services/asset-media.service.spec.ts +++ b/server/test/medium/specs/services/asset-media.service.spec.ts @@ -1,5 +1,7 @@ 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'; @@ -10,6 +12,7 @@ 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'; @@ -97,4 +100,162 @@ describe(AssetService.name, () => { }); }); }); + + 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/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/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/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.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 4847c84a35..55dcf6456f 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 v4(); @@ -159,11 +166,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 +236,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 +352,7 @@ const assetSidecarWriteFactory = () => { id: newUuid(), path: '/path/to/original-path.jpg.xmp', type: AssetFileType.Sidecar, + isEdited: false, }, ], exifInfo: { @@ -358,6 +380,7 @@ const assetOcrFactory = ( boxScore?: number; textScore?: number; text?: string; + isVisible?: boolean; } = {}, ) => ({ id: newUuid(), @@ -373,13 +396,120 @@ 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, +}); + export const factory = { activity: activityFactory, apiKey: apiKeyFactory, asset: assetFactory, + assetFile: assetFileFactory, assetOcr: assetOcrFactory, auth: authFactory, authApiKey: authApiKeyFactory, @@ -396,6 +526,11 @@ export const factory = { jobAssets: { sidecarWrite: assetSidecarWriteFactory, }, + exif: exifFactory, + face: faceFactory, + person: personFactory, + assetEdit: assetEditFactory, + tag: tagFactory, uuid: newUuid, date: newDate, responses: { diff --git a/server/test/utils.ts b/server/test/utils.ts index 77853f897a..cd866994eb 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -7,7 +7,7 @@ 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'; @@ -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'; @@ -216,6 +217,7 @@ export type ServiceOverrides = { app: AppRepository; audit: AuditRepository; asset: AssetRepository; + assetEdit: AssetEditRepository; assetJob: AssetJobRepository; config: ConfigRepository; cron: CronRepository; @@ -289,6 +291,7 @@ 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(), @@ -356,6 +359,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), @@ -492,6 +496,74 @@ export const mockSpawn = vitest.fn((exitCode: number, stdout: string, stderr: st } as unknown as ChildProcessWithoutNullStreams; }); +export const mockDuplex = vitest.fn( + (command: string, exitCode: number, stdout: string, stderr: string, error?: unknown) => { + const duplex = new Duplex({ + write(_chunk, _encoding, callback) { + 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/web/.nvmrc b/web/.nvmrc index 248216ad5b..3fe3b1570a 100644 --- a/web/.nvmrc +++ b/web/.nvmrc @@ -1 +1 @@ -24.12.0 +24.13.0 diff --git a/web/package.json b/web/package.json index acd888783f..6433a33dcc 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "2.4.1", + "version": "2.5.2", "license": "GNU Affero General Public License version 3", "type": "module", "scripts": { @@ -27,7 +27,7 @@ "@formatjs/icu-messageformat-parser": "^3.0.0", "@immich/justified-layout-wasm": "^0.4.3", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.54.0", + "@immich/ui": "^0.59.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.14.0", @@ -61,6 +61,7 @@ "svelte-persisted-store": "^0.12.0", "tabbable": "^6.2.0", "thumbhash": "^0.1.1", + "transformation-matrix": "^3.1.0", "uplot": "^1.6.32" }, "devDependencies": { @@ -71,7 +72,7 @@ "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/enhanced-img": "^0.9.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", @@ -97,7 +98,7 @@ "prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-svelte": "^3.3.3", "rollup-plugin-visualizer": "^6.0.0", - "svelte": "5.46.1", + "svelte": "5.48.0", "svelte-check": "^4.1.5", "svelte-eslint-parser": "^1.3.3", "tailwindcss": "^4.1.7", @@ -107,6 +108,6 @@ "vitest": "^3.0.0" }, "volta": { - "node": "24.12.0" + "node": "24.13.0" } } diff --git a/web/src/app.css b/web/src/app.css index bf7601f63b..dc2d3bf3c3 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); 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/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/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/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/components/AdminSidebar.svelte b/web/src/lib/components/AdminSidebar.svelte deleted file mode 100644 index fa660d7e2f..0000000000 --- a/web/src/lib/components/AdminSidebar.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - -
-
- - - - - -
- -
- -
-
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/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}
- +