diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..d1f60fa47 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +.vscode/ +cli/ +design/ +docker/ +docs/ +fastlane/ +machine-learning/ +misc/ +mobile/ + +server/node_modules +server/coverage/ +server/.reverse-geocoding-dump/ +server/upload/ +server/dist/ + +web/node_modules/ +web/coverage/ +web/.svelte-kit +web/build/ diff --git a/.gitattributes b/.gitattributes index 32ea167bb..986127fcb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,8 @@ mobile/openapi/**/*.dart linguist-generated=true mobile/openapi/.openapi-generator/FILES -diff -merge mobile/openapi/.openapi-generator/FILES linguist-generated=true +mobile/lib/**/*.g.dart -diff -merge +mobile/lib/**/*.g.dart linguist-generated=true cli/src/api/open-api/**/*.md -diff -merge cli/src/api/open-api/**/*.md linguist-generated=true @@ -15,3 +17,5 @@ web/src/api/open-api/**/*.md -diff -merge web/src/api/open-api/**/*.md linguist-generated=true web/src/api/open-api/**/*.ts -diff -merge web/src/api/open-api/**/*.ts linguist-generated=true + +*.sh text eol=lf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 28b2b7924..0d8bd637d 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,5 +1,5 @@ # These are supported funding model platforms -github: alextran1502 +github: immich-app liberapay: alex.tran1502 custom: https://www.buymeacoffee.com/altran1502 diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 4a69f48a0..b2ff4f41f 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -20,7 +20,7 @@ jobs: name: Build and sign Android # Skip when PR from a fork if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }} - runs-on: macos-12 + runs-on: macos-13 steps: - name: Determine ref @@ -35,7 +35,7 @@ jobs: with: ref: ${{ steps.get-ref.outputs.ref }} - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: "zulu" java-version: "12.x" diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml index 3cfaf0b2f..8b89cba10 100644 --- a/.github/workflows/cache-cleanup.yml +++ b/.github/workflows/cache-cleanup.yml @@ -1,4 +1,4 @@ -name: Clean up actions cache on PR close +name: Cache Cleanup on: pull_request: types: @@ -10,6 +10,7 @@ concurrency: jobs: cleanup: + name: Cleanup runs-on: ubuntu-latest steps: - name: Check out code diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml new file mode 100644 index 000000000..c66dcfb3c --- /dev/null +++ b/.github/workflows/cli-release.yml @@ -0,0 +1,23 @@ +name: CLI Release +on: + workflow_dispatch: + +jobs: + publish: + name: Publish + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./cli + + steps: + - uses: actions/checkout@v4 + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v4 + with: + node-version: "20.x" + registry-url: "https://registry.npmjs.org" + - run: npm ci + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/dispatch_sdk_update.yml b/.github/workflows/dispatch_sdk_update.yml index 06a29faf5..4f1abb5c4 100644 --- a/.github/workflows/dispatch_sdk_update.yml +++ b/.github/workflows/dispatch_sdk_update.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest if: ${{ !github.event.pull_request.head.repo.fork }} steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v7 with: github-token: ${{ secrets.GH_TOKEN }} script: | diff --git a/.github/workflows/docker-cleanup.yml b/.github/workflows/docker-cleanup.yml index 6edcef91f..a49ba5591 100644 --- a/.github/workflows/docker-cleanup.yml +++ b/.github/workflows/docker-cleanup.yml @@ -5,7 +5,7 @@ # # This workflow will not trigger runs on forked repos. -name: Cleanup Old Docker Images +name: Docker Cleanup on: pull_request: @@ -29,16 +29,13 @@ jobs: include: - primary-name: "immich-server" - primary-name: "immich-machine-learning" - - primary-name: "immich-web" - - primary-name: "immich-proxy" env: # Requires a personal access token with the OAuth scope delete:packages TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }} steps: - - - name: Clean temporary images + - name: Clean temporary images if: "${{ env.TOKEN != '' }}" - uses: stumpylog/image-cleaner-action/ephemeral@v0.3.0 + uses: stumpylog/image-cleaner-action/ephemeral@v0.4.0 with: token: "${{ env.TOKEN }}" owner: "immich-app" @@ -60,17 +57,14 @@ jobs: include: - primary-name: "immich-server" - primary-name: "immich-machine-learning" - - primary-name: "immich-web" - - primary-name: "immich-proxy" - primary-name: "immich-build-cache" env: # Requires a personal access token with the OAuth scope delete:packages TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }} steps: - - - name: Clean untagged images + - name: Clean untagged images if: "${{ env.TOKEN != '' }}" - uses: stumpylog/image-cleaner-action/untagged@v0.3.0 + uses: stumpylog/image-cleaner-action/untagged@v0.4.0 with: token: "${{ env.TOKEN }}" owner: "immich-app" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 3da18aa43..4bebc7502 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,4 +1,4 @@ -name: Build and Push Docker Images +name: Docker on: workflow_dispatch: @@ -18,106 +18,22 @@ permissions: jobs: build_and_push: + name: Build and Push runs-on: ubuntu-latest strategy: # Prevent a failure in one image from stopping the other builds fail-fast: false matrix: include: - - context: "web" - image: "immich-web" - platforms: "linux/amd64,linux/arm64" - context: "machine-learning" + file: "machine-learning/Dockerfile" image: "immich-machine-learning" platforms: "linux/amd64,linux/arm64" - - context: "nginx" - image: "immich-proxy" - platforms: "linux/amd64,linux/arm64" - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3.0.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 - # Workaround to fix error: - # failed to push: failed to copy: io: read/write on closed pipe - # See https://github.com/docker/build-push-action/issues/761 - with: - driver-opts: | - image=moby/buildkit:v0.10.6 - - - name: Login to Docker Hub - # Only push to Docker Hub when making a release - if: ${{ github.event_name == 'release' }} - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - # Skip when PR from a fork - if: ${{ !github.event.pull_request.head.repo.fork }} - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate docker image tags - id: metadata - uses: docker/metadata-action@v5 - with: - flavor: | - # Disable latest tag - latest=false - images: | - name=ghcr.io/${{ github.repository_owner }}/${{matrix.image}} - name=altran1502/${{matrix.image}},enable=${{ github.event_name == 'release' }} - tags: | - # Tag with branch name - type=ref,event=branch - # Tag with pr-number - type=ref,event=pr - # Tag with git tag on release - type=ref,event=tag - type=raw,value=release,enable=${{ github.event_name == 'release' }} - - - name: Determine build cache output - id: cache-target - run: | - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - # Essentially just ignore the cache output (PR can't write to registry cache) - echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT - else - echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ matrix.image }}" >> $GITHUB_OUTPUT - fi - - - name: Build and push image - uses: docker/build-push-action@v5.0.0 - with: - context: ${{ matrix.context }} - platforms: ${{ matrix.platforms }} - # Skip pushing when PR from a fork - push: ${{ !github.event.pull_request.head.repo.fork }} - cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}} - cache-to: ${{ steps.cache-target.outputs.cache-to }} - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - - build_and_push_server_arm_64: - runs-on: self-hosted - strategy: - # Prevent a failure in one image from stopping the other builds - fail-fast: false - matrix: - include: - - context: "server" + - context: "." + file: "server/Dockerfile" image: "immich-server" platforms: "linux/arm64,linux/amd64" + steps: - name: Checkout uses: actions/checkout@v4 @@ -181,9 +97,10 @@ jobs: fi - name: Build and push image - uses: docker/build-push-action@v5.0.0 + uses: docker/build-push-action@v5.1.0 with: context: ${{ matrix.context }} + file: ${{ matrix.file }} platforms: ${{ matrix.platforms }} # Skip pushing when PR from a fork push: ${{ !github.event.pull_request.head.repo.fork }} diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 18343c06f..661287252 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -32,3 +32,8 @@ jobs: - name: Run dart analyze run: dart analyze --fatal-infos working-directory: ./mobile + + # Enable after riverpod generator migration is completed + # - name: Run dart custom lint + # run: dart run custom_lint + # working-directory: ./mobile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 58efec377..2505aa185 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ concurrency: jobs: e2e-tests: - name: Run end-to-end test suites + name: Server (e2e) runs-on: ubuntu-latest steps: @@ -24,7 +24,7 @@ jobs: run: docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build doc-tests: - name: Run documentation checks + name: Docs runs-on: ubuntu-latest defaults: run: @@ -45,8 +45,12 @@ jobs: run: npm run check if: ${{ !cancelled() }} + - name: Run build + run: npm run build + if: ${{ !cancelled() }} + server-unit-tests: - name: Run server unit test suites and checks + name: Server runs-on: ubuntu-latest defaults: run: @@ -76,7 +80,7 @@ jobs: if: ${{ !cancelled() }} cli-unit-tests: - name: Run cli test suites + name: CLI runs-on: ubuntu-latest defaults: run: @@ -97,12 +101,16 @@ jobs: run: npm run format if: ${{ !cancelled() }} + - name: Run tsc + run: npm run check + if: ${{ !cancelled() }} + - name: Run unit tests & coverage run: npm run test:cov if: ${{ !cancelled() }} web-unit-tests: - name: Run web unit test suites and checks + name: Web runs-on: ubuntu-latest defaults: run: @@ -136,7 +144,7 @@ jobs: # if: ${{ !cancelled() }} mobile-unit-tests: - name: Run mobile unit tests + name: Mobile runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -150,7 +158,7 @@ jobs: run: flutter test -j 1 ml-unit-tests: - name: Run ML unit tests and checks + name: Machine Learning runs-on: ubuntu-latest defaults: run: @@ -168,19 +176,19 @@ jobs: poetry install --with dev - name: Lint with ruff run: | - poetry run ruff check --format=github app + poetry run ruff check --format=github app export - name: Check black formatting run: | - poetry run black --check app + poetry run black --check app export - name: Run mypy type checking run: | - poetry run mypy --install-types --non-interactive app/ + poetry run mypy --install-types --non-interactive --strict app/ export/ - name: Run tests and coverage run: | poetry run pytest --cov app generated-api-up-to-date: - name: Check generated files are up-to-date + name: OpenAPI Clients runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -201,11 +209,11 @@ jobs: exit 1 generated-typeorm-migrations-up-to-date: - name: Check generated TypeORM migrations are up-to-date + name: TypeORM Checks runs-on: ubuntu-latest services: postgres: - image: postgres + image: postgres@sha256:71da05df8c4f1e1bac9b92ebfba2a0eeb183f6ac6a972fd5e55e8146e29efe9c env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres @@ -228,7 +236,7 @@ jobs: - name: Install server dependencies run: npm ci - - name: Build the + - name: Build the app run: npm run build - name: Run existing migrations @@ -244,13 +252,30 @@ jobs: with: files: | server/src/infra/migrations/ - - name: Verify files have not changed + - name: Verify migration files have not changed if: steps.verify-changed-files.outputs.files_changed == 'true' run: | - echo "ERROR: Generated files not up to date!" + echo "ERROR: Generated migration files not up to date!" echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}" exit 1 + - name: Run SQL generation + run: npm run sql:generate + + - name: Find file changes + uses: tj-actions/verify-changed-files@v13.1 + id: verify-changed-sql-files + with: + files: | + server/src/infra/sql + + - name: Verify SQL files have not changed + if: steps.verify-changed-sql-files.outputs.files_changed == 'true' + run: | + echo "ERROR: Generated SQL files not up to date!" + echo "Changed files: ${{ steps.verify-changed-sql-files.outputs.changed_files }}" + exit 1 + # mobile-integration-tests: # name: Run mobile end-to-end integration tests # runs-on: macos-latest diff --git a/Makefile b/Makefile index eaf3c17f3..73b922ee8 100644 --- a/Makefile +++ b/Makefile @@ -1,38 +1,35 @@ dev: - docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans - -dev-new: docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans || make dev-down dev-down: docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans -dev-new-update: +dev-update: docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans -dev-update: - docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans - dev-scale: - docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans + docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans stage: - docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans + docker compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans pull-stage: - docker-compose -f ./docker/docker-compose.staging.yml pull + docker compose -f ./docker/docker-compose.staging.yml pull test-e2e: docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build prod: - docker-compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans + docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans prod-scale: - docker-compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans + docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans api: - cd ./server && npm run api:generate + npm --prefix server run api:generate + +sql: + npm --prefix server run sql:generate attach-server: docker exec -it docker_immich-server_1 sh diff --git a/README.md b/README.md index d4d38d0d5..7c884ef97 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
License: MIT - + Discord

@@ -18,14 +18,16 @@

- 中文 - Türkçe Català Español Français - Nederlands - 日本語 Italiano + 日本語 + 한국어 + Deutsch + Nederlands + Türkçe + 中文

## Disclaimer @@ -85,7 +87,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM | Virtual scroll | Yes | Yes | | OAuth support | Yes | Yes | | API Keys | N/A | Yes | -| LivePhoto backup and playback | iOS | Yes | +| LivePhoto/MotionPhoto backup and playback | Yes | Yes | | User-defined storage structure | Yes | Yes | | Public Sharing | No | Yes | | Archive and Favorites | Yes | Yes | @@ -95,6 +97,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM | Memories (x years ago) | Yes | Yes | | Offline support | Yes | No | | Read-only gallery | Yes | Yes | +| Stacked Photos | Yes | Yes | ## Support the project @@ -111,6 +114,7 @@ If you feel like this is the right cause and the app is something you are seeing - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz ## Contributors diff --git a/README_ca_ES.md b/README_ca_ES.md index 05cd6ef9a..d15c0fee9 100644 --- a/README_ca_ES.md +++ b/README_ca_ES.md @@ -19,13 +19,15 @@

English - 中文 - Türkçe Español Français - Nederlands - 日本語 Italiano + 日本語 + 한국어 + Deutsch + Nederlands + Türkçe + 中文

## Avís legal @@ -109,4 +111,4 @@ Si creieu que aquesta és una causa justa i l'aplicació és alguna cosa que us - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX - +- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_de_DE.md b/README_de_DE.md new file mode 100644 index 000000000..9515fff90 --- /dev/null +++ b/README_de_DE.md @@ -0,0 +1,122 @@ +

+
+ Lizenz: MIT + + Discord + +
+
+

+ +

+ +

+

Immich - Hoch performante, selbst gehostete Backup-Lösung für Fotos und Videos

+
+ + + +
+

+ English + Català + Español + Français + Italiano + 日本語 + 한국어 + Nederlands + Türkçe + 中文 +

+ +## Warnung + +- ⚠️ Das Projekt befindet sich in **sehr aktiver** Entwicklung. +- ⚠️ Erwarte Fehler und Änderungen mit Breaking-Changes. +- ⚠️ **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! + +## Inhalt + +- [Offizielle Dokumentation](https://immich.app/docs) +- [Roadmap](https://github.com/orgs/immich-app/projects/1) +- [Demo](#demo) +- [Funktionen](#funktionen) +- [Einführung](https://immich.app/docs/overview/introduction) +- [Installation](https://immich.app/docs/install/requirements) +- [Beitragsrichtlinien](https://immich.app/docs/overview/support-the-project) +- [Unterstütze das Projekt](#unterstütze-das-projekt) + +## Dokumentation + +Die Hauptdokumentation, inklusive Installationsanleitungen, ist unter https://immich.app zu finden. + +## Demo + +Die Web-Demo kannst Du unter https://demo.immich.app finden. + +Für die Handy-App kannst Du `https://demo.immich.app/api` als `Server Endpoint URL` angeben. + +```bash title="Demo Credential" +Die Anmeldedaten +email: demo@immich.app +passwort: demo +``` + +``` +Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM +``` + +## Funktionen + +| Funktionen | Mobil | Web | +| ---------------------------------------------------- | ------ | ----- | +| Fotos & Videos hochladen und ansehen | Ja | Ja | +| Automatisches Backup wenn die App geöffnet ist | Ja | n. a. | +| Selektive Auswahl von Alben zum Sichern | Ja | n. a. | +| Fotos und Videos auf das Gerät herunterladen | Ja | Ja | +| Unterstützt mehrere Benutzer | Ja | Ja | +| Album und geteilte Alben | Ja | Ja | +| Scrollleiste | Ja | Ja | +| Unterstützt RAW Formate | Ja | Ja | +| Metadaten anzeigen (EXIF, Karte) | Ja | Ja | +| Suchen nach Metadaten, Objekten, Gesichtern und CLIP | Ja | Ja | +| Administrative Funktionen (Benutzerverwaltung) | Nein | Ja | +| Backup im Hintergrund | Ja | n. a. | +| Virtuelles Scrollen | Ja | Ja | +| OAuth Unterstützung | Ja | Ja | +| API-Schlüssel | n. a. | Ja | +| LivePhoto/MotionPhoto Backup und Wiedergabe | Ja | Ja | +| Benutzerdefinierte Speicherstruktur | Ja | Ja | +| Öffentliches Teilen | Nein | Ja | +| Archive und Favoriten | Ja | Ja | +| Globale Karte | Ja | Ja | +| Teilen mit Partner | Ja | Ja | +| Gesichtserkennung und Gruppierung | Ja | Ja | +| Rückblicke (heute vor x Jahren) | Ja | Ja | +| Offline Unterstützung | Ja | Nein | +| Schreibgeschützte Gallerie | Ja | Ja | +| Gestapelte Bilder | Ja | Ja | + +## Unterstütze das Projekt + +Ich habe mich diesem Projekt verpflichtet und werde nicht aufgeben. Ich werde die Dokumentation weiter aktualisieren, neue Funktionen hinzufügen und Fehler beheben. Allerdings kann ich das nicht alleine schaffen. Daher brauche ich Eure Unterstützung, um mir zusätzliche Motivation zu geben, weiterzumachen. + +Wie unsere Gastgeber in der [selfhosted.show - In der Episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) gesagt haben, ist dies ein riesiges Unterfangen, welchem das Team und ich uns annehmen. In Zukunft würde ich liebend gerne Vollzeit an dem Projekt arbeiten und bitte daher um Eure Unterstützung. + +Wenn Du denkst, dass dies die richtige Sache ist und dich selbst die App für eine längere Zeit nutzen siehst, dann denke bitte darüber nach, das Projekt mit einer der unten aufgelisteten Optionen zu unterstützen. + +### Spenden + +- [Monatliche Spende](https://github.com/sponsors/immich-app) via GitHub Sponsors +- [Einmalige Spende](https://github.com/sponsors/immich-app?frequency=one-time&sponsor=immich-app) via GitHub Sponsors +- [Librepay](https://liberapay.com/alex.tran1502/) +- [buymeacoffee](https://www.buymeacoffee.com/altran1502) +- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz + +## Mitwirkende + + + diff --git a/README_es_ES.md b/README_es_ES.md index 5f88e9e37..c7a6f6910 100644 --- a/README_es_ES.md +++ b/README_es_ES.md @@ -19,12 +19,15 @@

English - 中文 - Türkçe Català Français - 日本語 Italiano + 日本語 + 한국어 + Deutsch + Nederlands + Türkçe + 中文

## Descargo de responsabilidad @@ -109,3 +112,4 @@ Si consideras que esta es una causa justa y la aplicación es algo que te gustar - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_fr_FR.md b/README_fr_FR.md index b986b85f3..93d01edfc 100644 --- a/README_fr_FR.md +++ b/README_fr_FR.md @@ -18,14 +18,16 @@

- 中文 - Türkçe + English Català Español - Français - Nederlands - 日本語 Italiano + 日本語 + 한국어 + Deutsch + Nederlands + Türkçe + 中文

## Clause de non-responsabilité @@ -111,3 +113,4 @@ Si vous estimez que c'est pour la bonne cause et que vous prévoyez d'utiliser l - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_it_IT.md b/README_it_IT.md index 8eb342bc4..0c7d20d73 100644 --- a/README_it_IT.md +++ b/README_it_IT.md @@ -19,13 +19,15 @@

English - 中文 - Türkçe Català Español Français - Nederlands 日本語 + 한국어 + Deutsch + Nederlands + Türkçe + 中文

## Declino di responsabilità @@ -111,3 +113,4 @@ Se pensi che Immich sia una buona causa e che l'app sia qualcosa che useresti ne - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_ja_JP.md b/README_ja_JP.md index 30661068b..557e0e358 100644 --- a/README_ja_JP.md +++ b/README_ja_JP.md @@ -18,13 +18,16 @@

- 中文 - Türkçe + English Català Español Français - Nederlands Italiano + 한국어 + Deutsch + Nederlands + Türkçe + 中文

## 免責事項 diff --git a/README_ko_KR.md b/README_ko_KR.md new file mode 100644 index 000000000..a6a49ae3d --- /dev/null +++ b/README_ko_KR.md @@ -0,0 +1,117 @@ +

+
+ License: MIT + + Discord + +
+
+

+ +

+ +

+

Immich - 고성능 자체 호스팅 사진 및 동영상 백업 솔루션

+
+ + + +
+

+ English + Català + Español + Français + Italiano + 日本語 + Deutsch + Nederlands + Türkçe + 中文 +

+ +## 주의 사항 + +- ⚠️ 이 프로젝트는 **매우 활발히** 개발 중입니다. +- ⚠️ 버그 및 잦은 변경 사항이 있을 수 있습니다. +- ⚠️ **사진과 동영상을 저장하는 유일한 방법으로 사용하지 마세요.** +- ⚠️ 중요한 사진과 동영상을 위해 항상 [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) 백업 계획을 따르세요! + +## 목차 + +- [공식 문서](https://immich.app/docs) +- [로드맵](https://github.com/orgs/immich-app/projects/1) +- [데모](#demo) +- [기능](#features) +- [소개](https://immich.app/docs/overview/introduction) +- [설치](https://immich.app/docs/install/requirements) +- [기여 가이드](https://immich.app/docs/overview/support-the-project) +- [프로젝트 지원](#support-the-project) + +## 문서 + +설치 가이드를 포함한 주요 문서는 https://immich.app 에서 확인할 수 있습니다. + +## 데모 + +https://demo.immich.app 에서 웹 데모를 체험할 수 있습니다. + +모바일 앱의 경우 `서버 엔드포인트 URL`에 `https://demo.immich.app`를 입력합니다. + +```bash title="Demo Credential" +자격 증명 +email: demo@immich.app +password: demo +``` + +``` +사양: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM +``` + +## 기능 + +| 기능 | 모바일 | 웹 | +| ------------------------------------ | ----- | ----- | +| 사진, 동영상 업로드 및 보기 | 예 | 예 | +| 앱을 열 때 자동으로 백업 | 예 | N/A | +| 백업용 앨범 선택 | 예 | N/A | +| 로컬 기기로 사진 및 동영상 다운로드 | 예 | 예 | +| 다른 사용자 추가 | 예 | 예 | +| 앨범 및 공유 앨범 | 예 | 예 | +| 스와이프/드래그 가능한 스크롤 바 | 예 | 예 | +| RAW 포맷 지원 | 예 | 예 | +| 메타데이터 보기 (EXIF, 위치) | 예 | 예 | +| 메타데이터, 사물, 얼굴 및 클립으로 검색 | 예 | 예 | +| 관리 기능 (사용자 관리) | 아니요 | 예 | +| 백그라운드 백업 | 예 | N/A | +| 가상 스크롤 | 예 | 예 | +| OAuth 지원 | 예 | 예 | +| API 키 | N/A | 예 | +| 라이브 포토/모션 포토 백업 및 재생 | 예 | 예 | +| 사용자 정의 스토리지 구조 | 예 | 예 | +| 모든 사용자와 공유 | 아니요 | 예 | +| 아카이브 및 즐겨찾기 | 예 |예| +| 글로벌 지도 | 예 | 예 | +| 특정 사용자와 공유 | 예 | 예 | +| 얼굴 인식 및 클러스터링 | 예 | 예 | +| 추억 (~년 전) | 예 | 예 | +| 오프라인 지원 | 예 | 아니요 | +| 읽기 전용 갤러리 | 예 | 예 | +| 사진 스택 | 예 | 예 | + +## 프로젝트 지원 + +저는 이 프로젝트에 전념해왔고, 앞으로도 멈추지 않을 것입니다. 문서를 업데이트하고, 새로운 기능을 추가하고, 버그를 수정하려 합니다. 하지만 혼자서는 할 수 없습니다. 계속해서 나아갈 수 있는 추가적인 동기부여를 위해 당신의 도움이 필요합니다. + +[selfhosted.show - In the episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) 진행자가 말했듯이, 우리가 하고 있는 것은 대규모 프로젝트입니다. 언젠가는 이 일을 풀타임으로 하는 것을 희망하며, 이를 실현하기 위해 당신의 도움이 필요합니다. + +만약 이에 동의하거나 이 앱을 장기간 사용하고자 한다면, 아래의 수단을 통해 이 프로젝트를 지원해 주세요. + +### 후원 + +- GitHub 스폰서를 통한 [정기 후원](https://github.com/sponsors/alextran1502) +- GitHub 스폰서를 통한 [일시 후원](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) +- [Librepay](https://liberapay.com/alex.tran1502/) +- [buymeacoffee](https://www.buymeacoffee.com/altran1502) +- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_nl_NL.md b/README_nl_NL.md index 1980b82a3..129d84ebc 100644 --- a/README_nl_NL.md +++ b/README_nl_NL.md @@ -18,14 +18,16 @@

- 中文 - Türkçe + English Català Español Français - Nederlands - 日本語 Italiano + 日本語 + 한국어 + Deutsch + Türkçe + 中文

## Disclaimer @@ -111,3 +113,4 @@ Als je denkt dat dit het juiste doel is en de app iets is dat je jezelf al heel - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_tr_TR.md b/README_tr_TR.md index b36949444..c5d221182 100644 --- a/README_tr_TR.md +++ b/README_tr_TR.md @@ -19,13 +19,15 @@

English - 中文 Català Español Français - Nederlands - 日本語 Italiano + 日本語 + 한국어 + Deutsch + Nederlands + 中文

## Feragatname @@ -108,3 +110,4 @@ Eğer bu size doğru bir amaç gibi geliyorsa ve uygulamanın uzun bir süre boy - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX +- ZCash: u1smm4wvqegcp46zss2jf5xptchgeczp4rx7a0wu3mermf2wxahm26yyz5w9mw3f2p4emwlljxjumg774kgs8rntt9yags0whnzane4n67z4c7gppq4yyvcj404ne3r769prwzd9j8ntvqp44fa6d67sf7rmcfjmds3gmeceff4u8e92rh38nd30cr96xw6vfhk6scu4ws90ldzupr3sz diff --git a/README_zh_CN.md b/README_zh_CN.md index fdd6777d6..16d4248ea 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -23,16 +23,17 @@

English - Türkçe Català Español Français - Nederlands - 日本語 Italiano + 日本語 + 한국어 + Deutsch + Nederlands + Türkçe

- ## 免责声明 - ⚠️ 本项目正在 **非常活跃** 地开发中。 diff --git a/cli/.npmignore b/cli/.npmignore new file mode 100644 index 000000000..e001747ce --- /dev/null +++ b/cli/.npmignore @@ -0,0 +1,10 @@ +**/*.spec.js +.editorconfig +.eslintignore +.eslintrc.js +.prettierignore +.prettierrc +package-lock.json +testSetup.js +tsconfig.json +tsconfig.build.json diff --git a/nginx/LICENSE b/cli/LICENSE similarity index 100% rename from nginx/LICENSE rename to cli/LICENSE diff --git a/cli/README.md b/cli/README.md index a11ade33b..a570a5523 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,46 +1,19 @@ -A command-line interface for interfacing with Immich +A command-line interface for interfacing with the self-hosted photo manager [Immich](https://immich.app/). -# Getting started +Please see the [Immich CLI documentation](https://immich.app/docs/features/command-line-interface). - $ ts-node cli/src +# For developers -To start using the CLI, you need to login with an API key first: +To run the Immich CLI from source, run the following in the cli folder: - $ ts-node cli/src login-key https://your-immich-instance/api your-api-key + $ npm run build + $ ts-node . -NOTE: This will store your api key under ~/.config/immich/auth.yml +You'll need ts-node, the easiest way to install it is to use npm: -Next, you can run commands: + $ npm i -g ts-node - $ ts-node cli/src server-info +You can also build and install the CLI using -When you're done, log out to remove the credentials from your filesystem - - $ ts-node cli/src logout - -# Usage - -``` -Usage: immich [options] [command] - -Immich command line interface - -Options: - -h, --help display help for command - -Commands: - upload [options] [paths...] Upload assets - import [options] [paths...] Import existing assets - server-info Display server information - login-key [instanceUrl] [apiKey] Login using an API key - help [command] display help for command -``` - -# Todo - -- Sidecar should check both .jpg.xmp and .xmp -- Sidecar check could be case-insensitive - -# Known issues - -- Upload can't use sdk due to multiple issues + $ npm run build + $ npm install -g . diff --git a/cli/package-lock.json b/cli/package-lock.json index e99e0c179..a855a2d8e 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,21 +1,25 @@ { - "name": "immich-cli", + "name": "@immich/cli", + "version": "2.0.4", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "immich-cli", + "name": "@immich/cli", + "version": "2.0.4", + "license": "MIT", "dependencies": { - "axios": "^1.4.0", + "axios": "^1.6.2", "byte-size": "^8.1.1", "cli-progress": "^3.12.0", "commander": "^11.0.0", "form-data": "^4.0.0", "glob": "^10.3.1", - "picomatch": "^2.3.1", - "systeminformation": "^5.18.4", "yaml": "^2.3.1" }, + "bin": { + "immich": "dist/src/index.js" + }, "devDependencies": { "@types/byte-size": "^8.1.0", "@types/chai": "^4.3.5", @@ -25,14 +29,14 @@ "@types/mime-types": "^2.1.1", "@types/mock-fs": "^4.13.1", "@types/node": "^20.3.1", - "@typescript-eslint/eslint-plugin": "^5.60.1", - "@typescript-eslint/parser": "^5.48.1", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", "chai": "^4.3.7", "eslint": "^8.43.0", - "eslint-config-prettier": "^8.8.0", + "eslint-config-prettier": "^9.0.0", "eslint-plugin-jest": "^27.2.2", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-unicorn": "^47.0.0", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-unicorn": "^49.0.0", "jest": "^29.5.0", "jest-extended": "^4.0.0", "jest-message-util": "^29.5.0", @@ -42,7 +46,7 @@ "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "tslib": "^2.5.3", - "typescript": "^4.9.4" + "typescript": "^5.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -775,9 +779,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -798,21 +802,21 @@ } }, "node_modules/@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -834,9 +838,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@isaacs/cliui": { @@ -1399,6 +1403,26 @@ "node": ">=14" } }, + "node_modules/@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1489,21 +1513,21 @@ } }, "node_modules/@types/byte-size": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.0.tgz", - "integrity": "sha512-LCIlZh8vyx+I2fgRycE1D34c33QDppYY6quBYYoaOpQ1nGhJ/avSP2VlrAefVotjJxgSk6WkKo0rTcCJwGG7vA==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz", + "integrity": "sha512-jGyVzYu6avI8yuqQCNTZd65tzI8HZrLjKX9sdMqZrGWVlNChu0rf6p368oVEDCYJe5BMx2Ov04tD1wqtgTwGSA==", "dev": true }, "node_modules/@types/chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==", + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", + "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", "dev": true }, "node_modules/@types/cli-progress": { - "version": "3.11.3", - "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.3.tgz", - "integrity": "sha512-/+C9xAdVtc+g5yHHkGBThgAA8rYpi5B+2ve3wLtybYj0JHEBs57ivR4x/zGfSsplRnV+psE91Nfin1soNKqz5Q==", + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.5.tgz", + "integrity": "sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==", "dev": true, "dependencies": { "@types/node": "*" @@ -1543,9 +1567,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.5", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz", - "integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==", + "version": "29.5.10", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", + "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -1553,9 +1577,9 @@ } }, "node_modules/@types/js-yaml": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.6.tgz", - "integrity": "sha512-ACTuifTSIIbyksx2HTon3aFtCKWcID7/h3XEmRpDYdMCXxPbl+m9GteOJeaAkiAta/NJaSFuA7ahZ0NkwajDSw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", "dev": true }, "node_modules/@types/json-schema": { @@ -1565,25 +1589,28 @@ "dev": true }, "node_modules/@types/mime-types": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.2.tgz", - "integrity": "sha512-q9QGHMGCiBJCHEvd4ZLdasdqXv570agPsUW0CeIm/B8DzhxsYMerD0l3IlI+EQ1A2RWHY2mmM9x1YIuuWxisCg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", "dev": true }, "node_modules/@types/mock-fs": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.2.tgz", - "integrity": "sha512-mSIMAOjrNTVUFmZgJEigSIm+GlS4hbrk8U5+M8EB45uMrykKdN9TidjjSaOY1yFph2+TD7bsIfB4r+IrMYVyPQ==", + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz", + "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "20.8.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", - "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", - "dev": true + "version": "20.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz", + "integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/normalize-package-data": { "version": "2.4.2", @@ -1619,32 +1646,33 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", + "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/type-utils": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1652,26 +1680,126 @@ } } }, - "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "debug": "^4.3.4" + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1679,6 +1807,80 @@ } } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", @@ -1697,25 +1899,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", + "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/utils": "6.13.1", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1723,6 +1925,105 @@ } } }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/types": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", @@ -1806,6 +2107,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -1950,9 +2257,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -2080,6 +2387,27 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.44" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2173,6 +2501,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/byte-size": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", @@ -2402,9 +2745,9 @@ } }, "node_modules/commander": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", - "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "engines": { "node": ">=16" } @@ -2519,6 +2862,168 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "dependencies": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "dependencies": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-browser/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/default-browser/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/default-browser/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2637,18 +3142,19 @@ } }, "node_modules/eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.54.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -2691,9 +3197,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", - "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -2703,9 +3209,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.4.2.tgz", - "integrity": "sha512-3Nfvv3wbq2+PZlRTf2oaAWXWwbdBejFRBR2O8tAO67o+P8zno+QGbcDYaAXODlreXVg+9gvWhKKmG2rgfb8GEg==", + "version": "27.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.0.tgz", + "integrity": "sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^5.10.0" @@ -2728,33 +3234,41 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", + "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", "dev": true, "dependencies": { - "prettier-linter-helpers": "^1.0.0" + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" }, "engines": { - "node": ">=12.0.0" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/prettier" }, "peerDependencies": { - "eslint": ">=7.28.0", - "prettier": ">=2.0.0" + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "prettier": ">=3.0.0" }, "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, "eslint-config-prettier": { "optional": true } } }, "node_modules/eslint-plugin-unicorn": { - "version": "47.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-47.0.0.tgz", - "integrity": "sha512-ivB3bKk7fDIeWOUmmMm9o3Ax9zbMz1Bsza/R2qm46ufw4T6VBFBaJIR1uN3pCKSmSXm8/9Nri8V+iUut1NhQGA==", + "version": "49.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-49.0.0.tgz", + "integrity": "sha512-0fHEa/8Pih5cmzFW5L7xMEfUTvI9WKeQtjmKpTUmY+BiFCDxkxrTdnURJOHKykhtwIeyYsxnecbGvDCml++z4Q==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-validator-identifier": "^7.22.20", "@eslint-community/eslint-utils": "^4.4.0", "ci-info": "^3.8.0", "clean-regexp": "^1.0.0", @@ -2762,13 +3276,11 @@ "indent-string": "^4.0.0", "is-builtin-module": "^3.2.1", "jsesc": "^3.0.2", - "lodash": "^4.17.21", "pluralize": "^8.0.0", "read-pkg-up": "^7.0.1", - "regexp-tree": "^0.1.24", + "regexp-tree": "^0.1.27", "regjsparser": "^0.10.0", - "safe-regex": "^2.1.1", - "semver": "^7.3.8", + "semver": "^7.5.4", "strip-indent": "^3.0.0" }, "engines": { @@ -2778,7 +3290,7 @@ "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" }, "peerDependencies": { - "eslint": ">=8.38.0" + "eslint": ">=8.52.0" } }, "node_modules/eslint-scope": { @@ -3481,6 +3993,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3519,6 +4046,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3549,6 +4094,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3867,9 +4439,9 @@ } }, "node_modules/jest-extended": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.1.tgz", - "integrity": "sha512-KM6dwuBUAgy6QONuR19CGubZB9Hkjqvl/d5Yc/FXsdB8+gsGxB2VQ+NEdOrr95J4GMPeLnDoPOKyi6+mKCCnZQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz", + "integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==", "dev": true, "dependencies": { "jest-diff": "^29.0.0", @@ -4400,12 +4972,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -4572,12 +5138,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4656,6 +5216,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -4825,6 +5403,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -5288,6 +5867,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5311,15 +5905,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", - "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", - "dev": true, - "dependencies": { - "regexp-tree": "~0.1.1" - } - }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -5611,29 +6196,20 @@ "integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==", "dev": true }, - "node_modules/systeminformation": { - "version": "5.21.9", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.9.tgz", - "integrity": "sha512-7pI4mu9P/2MGDV0T49B52E7IULBGj+kRVk6JSYUj5qfAk7N7C7aNX15fXziqrbgZntc6/jjYzWeb/x41jhg/eA==", - "os": [ - "darwin", - "linux", - "win32", - "freebsd", - "openbsd", - "netbsd", - "sunos", - "android" - ], - "bin": { - "systeminformation": "lib/cli.js" + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "dependencies": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" }, "engines": { - "node": ">=8.0.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { - "type": "Buy me a coffee", - "url": "https://www.buymeacoffee.com/systeminfo" + "url": "https://opencollective.com/unts" } }, "node_modules/test-exclude": { @@ -5676,6 +6252,18 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -5703,6 +6291,18 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", @@ -5850,16 +6450,31 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/update-browserslist-db": { @@ -6091,9 +6706,9 @@ "dev": true }, "node_modules/yaml": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", "engines": { "node": ">= 14" } @@ -6702,9 +7317,9 @@ "dev": true }, "@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -6719,18 +7334,18 @@ } }, "@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" } @@ -6742,9 +7357,9 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "@isaacs/cliui": { @@ -7180,6 +7795,20 @@ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "optional": true }, + "@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + } + }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -7270,21 +7899,21 @@ } }, "@types/byte-size": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.0.tgz", - "integrity": "sha512-LCIlZh8vyx+I2fgRycE1D34c33QDppYY6quBYYoaOpQ1nGhJ/avSP2VlrAefVotjJxgSk6WkKo0rTcCJwGG7vA==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz", + "integrity": "sha512-jGyVzYu6avI8yuqQCNTZd65tzI8HZrLjKX9sdMqZrGWVlNChu0rf6p368oVEDCYJe5BMx2Ov04tD1wqtgTwGSA==", "dev": true }, "@types/chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-VOVRLM1mBxIRxydiViqPcKn6MIxZytrbMpd6RJLIWKxUNr3zux8no0Oc7kJx0WAPIitgZ0gkrDS+btlqQpubpw==", + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", + "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", "dev": true }, "@types/cli-progress": { - "version": "3.11.3", - "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.3.tgz", - "integrity": "sha512-/+C9xAdVtc+g5yHHkGBThgAA8rYpi5B+2ve3wLtybYj0JHEBs57ivR4x/zGfSsplRnV+psE91Nfin1soNKqz5Q==", + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.5.tgz", + "integrity": "sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==", "dev": true, "requires": { "@types/node": "*" @@ -7324,9 +7953,9 @@ } }, "@types/jest": { - "version": "29.5.5", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz", - "integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==", + "version": "29.5.10", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", + "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", "dev": true, "requires": { "expect": "^29.0.0", @@ -7334,9 +7963,9 @@ } }, "@types/js-yaml": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.6.tgz", - "integrity": "sha512-ACTuifTSIIbyksx2HTon3aFtCKWcID7/h3XEmRpDYdMCXxPbl+m9GteOJeaAkiAta/NJaSFuA7ahZ0NkwajDSw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", "dev": true }, "@types/json-schema": { @@ -7346,25 +7975,28 @@ "dev": true }, "@types/mime-types": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.2.tgz", - "integrity": "sha512-q9QGHMGCiBJCHEvd4ZLdasdqXv570agPsUW0CeIm/B8DzhxsYMerD0l3IlI+EQ1A2RWHY2mmM9x1YIuuWxisCg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", "dev": true }, "@types/mock-fs": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.2.tgz", - "integrity": "sha512-mSIMAOjrNTVUFmZgJEigSIm+GlS4hbrk8U5+M8EB45uMrykKdN9TidjjSaOY1yFph2+TD7bsIfB4r+IrMYVyPQ==", + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz", + "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==", "dev": true, "requires": { "@types/node": "*" } }, "@types/node": { - "version": "20.8.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", - "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", - "dev": true + "version": "20.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz", + "integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } }, "@types/normalize-package-data": { "version": "2.4.2", @@ -7400,33 +8032,136 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", + "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", "dev": true, "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/type-utils": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" + } + }, + "@typescript-eslint/types": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/utils": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "semver": "^7.5.4" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" + } + } } }, "@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" + } + }, + "@typescript-eslint/types": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" + } + } } }, "@typescript-eslint/scope-manager": { @@ -7440,15 +8175,73 @@ } }, "@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", + "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/utils": "6.13.1", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" + } + }, + "@typescript-eslint/types": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/utils": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "semver": "^7.5.4" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" + } + } } }, "@typescript-eslint/types": { @@ -7498,6 +8291,12 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -7599,9 +8398,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -7704,6 +8503,21 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true + }, + "bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "requires": { + "big-integer": "^1.6.44" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -7765,6 +8579,15 @@ "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true }, + "bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "requires": { + "run-applescript": "^5.0.0" + } + }, "byte-size": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", @@ -7923,9 +8746,9 @@ } }, "commander": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", - "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==" + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" }, "concat-map": { "version": "0.0.1", @@ -8007,6 +8830,107 @@ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true }, + "default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "requires": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "dependencies": { + "execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + } + }, + "human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + } + } + }, + "default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "requires": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + } + }, + "define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -8092,18 +9016,19 @@ "dev": true }, "eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.54.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -8155,37 +9080,38 @@ } }, "eslint-config-prettier": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", - "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", "dev": true, "requires": {} }, "eslint-plugin-jest": { - "version": "27.4.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.4.2.tgz", - "integrity": "sha512-3Nfvv3wbq2+PZlRTf2oaAWXWwbdBejFRBR2O8tAO67o+P8zno+QGbcDYaAXODlreXVg+9gvWhKKmG2rgfb8GEg==", + "version": "27.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.0.tgz", + "integrity": "sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==", "dev": true, "requires": { "@typescript-eslint/utils": "^5.10.0" } }, "eslint-plugin-prettier": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", - "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", + "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", "dev": true, "requires": { - "prettier-linter-helpers": "^1.0.0" + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" } }, "eslint-plugin-unicorn": { - "version": "47.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-47.0.0.tgz", - "integrity": "sha512-ivB3bKk7fDIeWOUmmMm9o3Ax9zbMz1Bsza/R2qm46ufw4T6VBFBaJIR1uN3pCKSmSXm8/9Nri8V+iUut1NhQGA==", + "version": "49.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-49.0.0.tgz", + "integrity": "sha512-0fHEa/8Pih5cmzFW5L7xMEfUTvI9WKeQtjmKpTUmY+BiFCDxkxrTdnURJOHKykhtwIeyYsxnecbGvDCml++z4Q==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-validator-identifier": "^7.22.20", "@eslint-community/eslint-utils": "^4.4.0", "ci-info": "^3.8.0", "clean-regexp": "^1.0.0", @@ -8193,13 +9119,11 @@ "indent-string": "^4.0.0", "is-builtin-module": "^3.2.1", "jsesc": "^3.0.2", - "lodash": "^4.17.21", "pluralize": "^8.0.0", "read-pkg-up": "^7.0.1", - "regexp-tree": "^0.1.24", + "regexp-tree": "^0.1.27", "regjsparser": "^0.10.0", - "safe-regex": "^2.1.1", - "semver": "^7.3.8", + "semver": "^7.5.4", "strip-indent": "^3.0.0" } }, @@ -8695,6 +9619,12 @@ "has": "^1.0.3" } }, + "is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8721,6 +9651,15 @@ "is-extglob": "^2.1.1" } }, + "is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "requires": { + "is-docker": "^3.0.0" + } + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -8739,6 +9678,23 @@ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + }, + "dependencies": { + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + } + } + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -8969,9 +9925,9 @@ } }, "jest-extended": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.1.tgz", - "integrity": "sha512-KM6dwuBUAgy6QONuR19CGubZB9Hkjqvl/d5Yc/FXsdB8+gsGxB2VQ+NEdOrr95J4GMPeLnDoPOKyi6+mKCCnZQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz", + "integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==", "dev": true, "requires": { "jest-diff": "^29.0.0", @@ -9387,12 +10343,6 @@ "p-locate": "^5.0.0" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -9526,12 +10476,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -9597,6 +10541,18 @@ "mimic-fn": "^2.1.0" } }, + "open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "requires": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + } + }, "optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -9716,7 +10672,8 @@ "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true }, "pirates": { "version": "4.0.6", @@ -10034,6 +10991,15 @@ } } }, + "run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "requires": { + "execa": "^5.0.0" + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10043,15 +11009,6 @@ "queue-microtask": "^1.2.2" } }, - "safe-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", - "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", - "dev": true, - "requires": { - "regexp-tree": "~0.1.1" - } - }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -10273,10 +11230,15 @@ "integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==", "dev": true }, - "systeminformation": { - "version": "5.21.9", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.9.tgz", - "integrity": "sha512-7pI4mu9P/2MGDV0T49B52E7IULBGj+kRVk6JSYUj5qfAk7N7C7aNX15fXziqrbgZntc6/jjYzWeb/x41jhg/eA==" + "synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "requires": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + } }, "test-exclude": { "version": "6.0.0", @@ -10311,6 +11273,12 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -10332,6 +11300,13 @@ "is-number": "^7.0.0" } }, + "ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "requires": {} + }, "ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", @@ -10414,9 +11389,21 @@ "dev": true }, "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "dev": true + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true }, "update-browserslist-db": { @@ -10582,9 +11569,9 @@ "dev": true }, "yaml": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==" + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==" }, "yargs": { "version": "17.7.2", diff --git a/cli/package.json b/cli/package.json index ccb87cca8..a1550f7b9 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,14 +1,23 @@ { - "name": "immich-cli", + "name": "@immich/cli", + "version": "2.0.4", + "description": "Command Line Interface (CLI) for Immich", + "main": "dist/index.js", + "bin": { + "immich": "./dist/src/index.js" + }, + "license": "MIT", + "keywords": [ + "immich", + "cli" + ], "dependencies": { - "axios": "^1.4.0", + "axios": "^1.6.2", "byte-size": "^8.1.1", "cli-progress": "^3.12.0", "commander": "^11.0.0", "form-data": "^4.0.0", "glob": "^10.3.1", - "picomatch": "^2.3.1", - "systeminformation": "^5.18.4", "yaml": "^2.3.1" }, "devDependencies": { @@ -20,14 +29,14 @@ "@types/mime-types": "^2.1.1", "@types/mock-fs": "^4.13.1", "@types/node": "^20.3.1", - "@typescript-eslint/eslint-plugin": "^5.60.1", - "@typescript-eslint/parser": "^5.48.1", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", "chai": "^4.3.7", "eslint": "^8.43.0", - "eslint-config-prettier": "^8.8.0", + "eslint-config-prettier": "^9.0.0", "eslint-plugin-jest": "^27.2.2", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-unicorn": "^47.0.0", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-unicorn": "^49.0.0", "jest": "^29.5.0", "jest-extended": "^4.0.0", "jest-message-util": "^29.5.0", @@ -37,15 +46,17 @@ "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "tslib": "^2.5.3", - "typescript": "^4.9.4" + "typescript": "^5.0.0" }, "scripts": { "build": "tsc --project tsconfig.build.json", "lint": "eslint \"src/**/*.ts\" --max-warnings 0", - "prepack": "yarn build ", + "prepack": "npm run build", "test": "jest", "test:cov": "jest --coverage", - "format": "prettier --check ." + "format": "prettier --check .", + "format:fix": "prettier --write .", + "check": "tsc --noEmit" }, "jest": { "clearMocks": true, @@ -62,7 +73,15 @@ "collectCoverageFrom": [ "/src/**/*.(t|j)s" ], + "moduleNameMapper": { + "^@api(|/.*)$": "/src/api/$1" + }, "coverageDirectory": "./coverage", "testEnvironment": "node" + }, + "repository": { + "type": "git", + "url": "github:immich-app/immich", + "directory": "cli" } } diff --git a/cli/src/__mocks__/axios.ts b/cli/src/__mocks__/axios.ts deleted file mode 100644 index e447986d8..000000000 --- a/cli/src/__mocks__/axios.ts +++ /dev/null @@ -1,3 +0,0 @@ -// ./__mocks__/axios.js -import mockAxios from 'jest-mock-axios'; -export default mockAxios; diff --git a/cli/src/api/client.ts b/cli/src/api/client.ts index 713a6ee57..392e824b6 100644 --- a/cli/src/api/client.ts +++ b/cli/src/api/client.ts @@ -11,6 +11,7 @@ import { UserApi, } from './open-api'; import { ApiConfiguration } from '../cores/api-configuration'; +import FormData from 'form-data'; export class ImmichApi { public userApi: UserApi; @@ -35,6 +36,7 @@ export class ImmichApi { 'x-api-key': apiKey, }, }, + formDataCtor: FormData, }); this.userApi = new UserApi(this.config); diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 9265d439f..ac5ea101e 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.84.0 + * The version of the OpenAPI document: 1.89.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -209,43 +209,6 @@ export interface AddUsersDto { */ 'sharedUserIds': Array; } -/** - * - * @export - * @interface AdminSignupResponseDto - */ -export interface AdminSignupResponseDto { - /** - * - * @type {string} - * @memberof AdminSignupResponseDto - */ - 'createdAt': string; - /** - * - * @type {string} - * @memberof AdminSignupResponseDto - */ - 'email': string; - /** - * - * @type {string} - * @memberof AdminSignupResponseDto - */ - 'firstName': string; - /** - * - * @type {string} - * @memberof AdminSignupResponseDto - */ - 'id': string; - /** - * - * @type {string} - * @memberof AdminSignupResponseDto - */ - 'lastName': string; -} /** * * @export @@ -331,6 +294,12 @@ export interface AlbumResponseDto { * @memberof AlbumResponseDto */ 'id': string; + /** + * + * @type {boolean} + * @memberof AlbumResponseDto + */ + 'isActivityEnabled': boolean; /** * * @type {string} @@ -478,6 +447,12 @@ export interface AssetBulkDeleteDto { * @interface AssetBulkUpdateDto */ export interface AssetBulkUpdateDto { + /** + * + * @type {string} + * @memberof AssetBulkUpdateDto + */ + 'dateTimeOriginal'?: string; /** * * @type {Array} @@ -496,6 +471,18 @@ export interface AssetBulkUpdateDto { * @memberof AssetBulkUpdateDto */ 'isFavorite'?: boolean; + /** + * + * @type {number} + * @memberof AssetBulkUpdateDto + */ + 'latitude'?: number; + /** + * + * @type {number} + * @memberof AssetBulkUpdateDto + */ + 'longitude'?: number; /** * * @type {boolean} @@ -701,6 +688,20 @@ export interface AssetJobsDto { } +/** + * + * @export + * @enum {string} + */ + +export const AssetOrder = { + Asc: 'asc', + Desc: 'desc' +} as const; + +export type AssetOrder = typeof AssetOrder[keyof typeof AssetOrder]; + + /** * * @export @@ -1181,22 +1182,6 @@ export interface CheckExistingAssetsResponseDto { */ 'existingIds': Array; } -/** - * - * @export - * @enum {string} - */ - -export const CitiesFile = { - Cities15000: 'cities15000', - Cities5000: 'cities5000', - Cities1000: 'cities1000', - Cities500: 'cities500' -} as const; - -export type CitiesFile = typeof CitiesFile[keyof typeof CitiesFile]; - - /** * * @export @@ -1372,24 +1357,18 @@ export interface CreateUserDto { * @memberof CreateUserDto */ 'externalPath'?: string | null; - /** - * - * @type {string} - * @memberof CreateUserDto - */ - 'firstName': string; - /** - * - * @type {string} - * @memberof CreateUserDto - */ - 'lastName': string; /** * * @type {boolean} * @memberof CreateUserDto */ 'memoriesEnabled'?: boolean; + /** + * + * @type {string} + * @memberof CreateUserDto + */ + 'name': string; /** * * @type {string} @@ -1796,97 +1775,6 @@ export interface FileReportItemDto { } -/** - * - * @export - * @interface ImportAssetDto - */ -export interface ImportAssetDto { - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'assetPath': string; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'deviceAssetId': string; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'deviceId': string; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'duration'?: string; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'fileCreatedAt': string; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'fileModifiedAt': string; - /** - * - * @type {boolean} - * @memberof ImportAssetDto - */ - 'isArchived'?: boolean; - /** - * - * @type {boolean} - * @memberof ImportAssetDto - */ - 'isExternal'?: boolean; - /** - * - * @type {boolean} - * @memberof ImportAssetDto - */ - 'isFavorite'?: boolean; - /** - * - * @type {boolean} - * @memberof ImportAssetDto - */ - 'isOffline'?: boolean; - /** - * - * @type {boolean} - * @memberof ImportAssetDto - */ - 'isReadOnly'?: boolean; - /** - * - * @type {boolean} - * @memberof ImportAssetDto - */ - 'isVisible'?: boolean; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'libraryId'?: string; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'sidecarPath'?: string; -} /** * * @export @@ -2168,12 +2056,6 @@ export interface LoginResponseDto { * @memberof LoginResponseDto */ 'accessToken': string; - /** - * - * @type {string} - * @memberof LoginResponseDto - */ - 'firstName': string; /** * * @type {boolean} @@ -2185,7 +2067,7 @@ export interface LoginResponseDto { * @type {string} * @memberof LoginResponseDto */ - 'lastName': string; + 'name': string; /** * * @type {string} @@ -2255,6 +2137,20 @@ export interface MapMarkerResponseDto { */ 'lon': number; } +/** + * + * @export + * @enum {string} + */ + +export const MapTheme = { + Light: 'light', + Dark: 'dark' +} as const; + +export type MapTheme = typeof MapTheme[keyof typeof MapTheme]; + + /** * * @export @@ -2378,6 +2274,105 @@ export interface OAuthConfigResponseDto { */ 'url'?: string; } +/** + * + * @export + * @interface PartnerResponseDto + */ +export interface PartnerResponseDto { + /** + * + * @type {UserAvatarColor} + * @memberof PartnerResponseDto + */ + 'avatarColor': UserAvatarColor; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'createdAt': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'deletedAt': string | null; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'email': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'externalPath': string | null; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'id': string; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'inTimeline'?: boolean; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'isAdmin': boolean; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'memoriesEnabled'?: boolean; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'name': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'oauthId': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'profileImagePath': string; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'shouldChangePassword': boolean; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'storageLabel': string | null; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'updatedAt': string; +} + + /** * * @export @@ -2587,6 +2582,20 @@ export interface QueueStatusDto { */ 'isPaused': boolean; } +/** + * + * @export + * @enum {string} + */ + +export const ReactionLevel = { + Album: 'album', + Asset: 'asset' +} as const; + +export type ReactionLevel = typeof ReactionLevel[keyof typeof ReactionLevel]; + + /** * * @export @@ -2696,19 +2705,6 @@ export interface SearchAlbumResponseDto { */ 'total': number; } -/** - * - * @export - * @interface SearchAssetDto - */ -export interface SearchAssetDto { - /** - * - * @type {string} - * @memberof SearchAssetDto - */ - 'searchTerm': string; -} /** * * @export @@ -2853,12 +2849,6 @@ export interface ServerConfigDto { * @memberof ServerConfigDto */ 'loginPageMessage': string; - /** - * - * @type {string} - * @memberof ServerConfigDto - */ - 'mapTileUrl': string; /** * * @type {string} @@ -3343,13 +3333,7 @@ export interface SignUpDto { * @type {string} * @memberof SignUpDto */ - 'firstName': string; - /** - * - * @type {string} - * @memberof SignUpDto - */ - 'lastName': string; + 'name': string; /** * * @type {string} @@ -3726,6 +3710,12 @@ export interface SystemConfigMachineLearningDto { * @interface SystemConfigMapDto */ export interface SystemConfigMapDto { + /** + * + * @type {string} + * @memberof SystemConfigMapDto + */ + 'darkStyle': string; /** * * @type {boolean} @@ -3737,7 +3727,7 @@ export interface SystemConfigMapDto { * @type {string} * @memberof SystemConfigMapDto */ - 'tileUrl': string; + 'lightStyle': string; } /** * @@ -3844,12 +3834,6 @@ export interface SystemConfigPasswordLoginDto { * @interface SystemConfigReverseGeocodingDto */ export interface SystemConfigReverseGeocodingDto { - /** - * - * @type {CitiesFile} - * @memberof SystemConfigReverseGeocodingDto - */ - 'citiesFileOverride': CitiesFile; /** * * @type {boolean} @@ -3857,8 +3841,6 @@ export interface SystemConfigReverseGeocodingDto { */ 'enabled': boolean; } - - /** * * @export @@ -4160,6 +4142,12 @@ export interface UpdateAlbumDto { * @memberof UpdateAlbumDto */ 'description'?: string; + /** + * + * @type {boolean} + * @memberof UpdateAlbumDto + */ + 'isActivityEnabled'?: boolean; } /** * @@ -4167,6 +4155,12 @@ export interface UpdateAlbumDto { * @interface UpdateAssetDto */ export interface UpdateAssetDto { + /** + * + * @type {string} + * @memberof UpdateAssetDto + */ + 'dateTimeOriginal'?: string; /** * * @type {string} @@ -4185,6 +4179,18 @@ export interface UpdateAssetDto { * @memberof UpdateAssetDto */ 'isFavorite'?: boolean; + /** + * + * @type {number} + * @memberof UpdateAssetDto + */ + 'latitude'?: number; + /** + * + * @type {number} + * @memberof UpdateAssetDto + */ + 'longitude'?: number; } /** * @@ -4217,6 +4223,19 @@ export interface UpdateLibraryDto { */ 'name'?: string; } +/** + * + * @export + * @interface UpdatePartnerDto + */ +export interface UpdatePartnerDto { + /** + * + * @type {boolean} + * @memberof UpdatePartnerDto + */ + 'inTimeline': boolean; +} /** * * @export @@ -4255,6 +4274,12 @@ export interface UpdateTagDto { * @interface UpdateUserDto */ export interface UpdateUserDto { + /** + * + * @type {UserAvatarColor} + * @memberof UpdateUserDto + */ + 'avatarColor'?: UserAvatarColor; /** * * @type {string} @@ -4267,12 +4292,6 @@ export interface UpdateUserDto { * @memberof UpdateUserDto */ 'externalPath'?: string; - /** - * - * @type {string} - * @memberof UpdateUserDto - */ - 'firstName'?: string; /** * * @type {string} @@ -4285,18 +4304,18 @@ export interface UpdateUserDto { * @memberof UpdateUserDto */ 'isAdmin'?: boolean; - /** - * - * @type {string} - * @memberof UpdateUserDto - */ - 'lastName'?: string; /** * * @type {boolean} * @memberof UpdateUserDto */ 'memoriesEnabled'?: boolean; + /** + * + * @type {string} + * @memberof UpdateUserDto + */ + 'name'?: string; /** * * @type {string} @@ -4316,6 +4335,8 @@ export interface UpdateUserDto { */ 'storageLabel'?: string; } + + /** * * @export @@ -4334,12 +4355,6 @@ export interface UsageByUserDto { * @memberof UsageByUserDto */ 'usage': number; - /** - * - * @type {string} - * @memberof UsageByUserDto - */ - 'userFirstName': string; /** * * @type {string} @@ -4351,7 +4366,7 @@ export interface UsageByUserDto { * @type {string} * @memberof UsageByUserDto */ - 'userLastName': string; + 'userName': string; /** * * @type {number} @@ -4359,24 +4374,46 @@ export interface UsageByUserDto { */ 'videos': number; } +/** + * + * @export + * @enum {string} + */ + +export const UserAvatarColor = { + Primary: 'primary', + Pink: 'pink', + Red: 'red', + Yellow: 'yellow', + Blue: 'blue', + Green: 'green', + Purple: 'purple', + Orange: 'orange', + Gray: 'gray', + Amber: 'amber' +} as const; + +export type UserAvatarColor = typeof UserAvatarColor[keyof typeof UserAvatarColor]; + + /** * * @export * @interface UserDto */ export interface UserDto { + /** + * + * @type {UserAvatarColor} + * @memberof UserDto + */ + 'avatarColor': UserAvatarColor; /** * * @type {string} * @memberof UserDto */ 'email': string; - /** - * - * @type {string} - * @memberof UserDto - */ - 'firstName': string; /** * * @type {string} @@ -4388,7 +4425,7 @@ export interface UserDto { * @type {string} * @memberof UserDto */ - 'lastName': string; + 'name': string; /** * * @type {string} @@ -4396,12 +4433,20 @@ export interface UserDto { */ 'profileImagePath': string; } + + /** * * @export * @interface UserResponseDto */ export interface UserResponseDto { + /** + * + * @type {UserAvatarColor} + * @memberof UserResponseDto + */ + 'avatarColor': UserAvatarColor; /** * * @type {string} @@ -4426,12 +4471,6 @@ export interface UserResponseDto { * @memberof UserResponseDto */ 'externalPath': string | null; - /** - * - * @type {string} - * @memberof UserResponseDto - */ - 'firstName': string; /** * * @type {string} @@ -4444,18 +4483,18 @@ export interface UserResponseDto { * @memberof UserResponseDto */ 'isAdmin': boolean; - /** - * - * @type {string} - * @memberof UserResponseDto - */ - 'lastName': string; /** * * @type {boolean} * @memberof UserResponseDto */ 'memoriesEnabled'?: boolean; + /** + * + * @type {string} + * @memberof UserResponseDto + */ + 'name': string; /** * * @type {string} @@ -4487,6 +4526,8 @@ export interface UserResponseDto { */ 'updatedAt': string; } + + /** * * @export @@ -5076,11 +5117,12 @@ export const ActivityApiAxiosParamCreator = function (configuration?: Configurat * @param {string} albumId * @param {string} [assetId] * @param {ReactionType} [type] + * @param {ReactionLevel} [level] * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getActivities: async (albumId: string, assetId?: string, type?: ReactionType, userId?: string, options: AxiosRequestConfig = {}): Promise => { + getActivities: async (albumId: string, assetId?: string, type?: ReactionType, level?: ReactionLevel, userId?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'albumId' is not null or undefined assertParamExists('getActivities', 'albumId', albumId) const localVarPath = `/activity`; @@ -5116,6 +5158,10 @@ export const ActivityApiAxiosParamCreator = function (configuration?: Configurat localVarQueryParameter['type'] = type; } + if (level !== undefined) { + localVarQueryParameter['level'] = level; + } + if (userId !== undefined) { localVarQueryParameter['userId'] = userId; } @@ -5216,12 +5262,13 @@ export const ActivityApiFp = function(configuration?: Configuration) { * @param {string} albumId * @param {string} [assetId] * @param {ReactionType} [type] + * @param {ReactionLevel} [level] * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getActivities(albumId: string, assetId?: string, type?: ReactionType, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, userId, options); + async getActivities(albumId: string, assetId?: string, type?: ReactionType, level?: ReactionLevel, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, level, userId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -5270,7 +5317,7 @@ export const ActivityApiFactory = function (configuration?: Configuration, baseP * @throws {RequiredError} */ getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(axios, basePath)); + return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.level, requestParameters.userId, options).then((request) => request(axios, basePath)); }, /** * @@ -5339,6 +5386,13 @@ export interface ActivityApiGetActivitiesRequest { */ readonly type?: ReactionType + /** + * + * @type {ReactionLevel} + * @memberof ActivityApiGetActivities + */ + readonly level?: ReactionLevel + /** * * @type {string} @@ -5405,7 +5459,7 @@ export class ActivityApi extends BaseAPI { * @memberof ActivityApi */ public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) { - return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(this.axios, this.basePath)); + return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.level, requestParameters.userId, options).then((request) => request(this.axios, this.basePath)); } /** @@ -6766,6 +6820,48 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Get all asset of a device that are in the database, ID only. + * @param {string} deviceId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllUserAssetsByDeviceId: async (deviceId: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'deviceId' is not null or undefined + assertParamExists('getAllUserAssetsByDeviceId', 'deviceId', deviceId) + const localVarPath = `/asset/device/{deviceId}` + .replace(`{${"deviceId"}}`, encodeURIComponent(String(deviceId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -7258,11 +7354,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { + getTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'size' is not null or undefined assertParamExists('getTimeBucket', 'size', size) // verify required parameter 'timeBucket' is not null or undefined @@ -7320,6 +7417,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['withStacked'] = withStacked; } + if (withPartners !== undefined) { + localVarQueryParameter['withPartners'] = withPartners; + } + if (timeBucket !== undefined) { localVarQueryParameter['timeBucket'] = timeBucket; } @@ -7349,11 +7450,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { + getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'size' is not null or undefined assertParamExists('getTimeBuckets', 'size', size) const localVarPath = `/asset/time-buckets`; @@ -7409,6 +7511,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['withStacked'] = withStacked; } + if (withPartners !== undefined) { + localVarQueryParameter['withPartners'] = withPartners; + } + if (key !== undefined) { localVarQueryParameter['key'] = key; } @@ -7425,9 +7531,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }; }, /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {string} deviceId * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ getUserAssetsByDeviceId: async (deviceId: string, options: AxiosRequestConfig = {}): Promise => { @@ -7466,50 +7574,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, - /** - * - * @param {ImportAssetDto} importAssetDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - importFile: async (importAssetDto: ImportAssetDto, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'importAssetDto' is not null or undefined - assertParamExists('importFile', 'importAssetDto', importAssetDto) - const localVarPath = `/asset/import`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(importAssetDto, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {BulkIdsDto} bulkIdsDto @@ -7638,14 +7702,51 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }, /** * - * @param {SearchAssetDto} searchAssetDto + * @param {string} [id] + * @param {string} [libraryId] + * @param {AssetTypeEnum} [type] + * @param {AssetOrder} [order] + * @param {string} [deviceAssetId] + * @param {string} [deviceId] + * @param {string} [checksum] + * @param {boolean} [isArchived] + * @param {boolean} [isEncoded] + * @param {boolean} [isExternal] + * @param {boolean} [isFavorite] + * @param {boolean} [isMotion] + * @param {boolean} [isOffline] + * @param {boolean} [isReadOnly] + * @param {boolean} [isVisible] + * @param {boolean} [withDeleted] + * @param {boolean} [withStacked] + * @param {boolean} [withExif] + * @param {boolean} [withPeople] + * @param {string} [createdBefore] + * @param {string} [createdAfter] + * @param {string} [updatedBefore] + * @param {string} [updatedAfter] + * @param {string} [trashedBefore] + * @param {string} [trashedAfter] + * @param {string} [takenBefore] + * @param {string} [takenAfter] + * @param {string} [originalFileName] + * @param {string} [originalPath] + * @param {string} [resizePath] + * @param {string} [webpPath] + * @param {string} [encodedVideoPath] + * @param {string} [city] + * @param {string} [state] + * @param {string} [country] + * @param {string} [make] + * @param {string} [model] + * @param {string} [lensModel] + * @param {number} [page] + * @param {number} [size] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - searchAsset: async (searchAssetDto: SearchAssetDto, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'searchAssetDto' is not null or undefined - assertParamExists('searchAsset', 'searchAssetDto', searchAssetDto) - const localVarPath = `/asset/search`; + searchAssets: async (id?: string, libraryId?: string, type?: AssetTypeEnum, order?: AssetOrder, deviceAssetId?: string, deviceId?: string, checksum?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, withDeleted?: boolean, withStacked?: boolean, withExif?: boolean, withPeople?: boolean, createdBefore?: string, createdAfter?: string, updatedBefore?: string, updatedAfter?: string, trashedBefore?: string, trashedAfter?: string, takenBefore?: string, takenAfter?: string, originalFileName?: string, originalPath?: string, resizePath?: string, webpPath?: string, encodedVideoPath?: string, city?: string, state?: string, country?: string, make?: string, model?: string, lensModel?: string, page?: number, size?: number, options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/assets`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -7653,7 +7754,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -7666,14 +7767,187 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) + if (id !== undefined) { + localVarQueryParameter['id'] = id; + } + + if (libraryId !== undefined) { + localVarQueryParameter['libraryId'] = libraryId; + } + + if (type !== undefined) { + localVarQueryParameter['type'] = type; + } + + if (order !== undefined) { + localVarQueryParameter['order'] = order; + } + + if (deviceAssetId !== undefined) { + localVarQueryParameter['deviceAssetId'] = deviceAssetId; + } + + if (deviceId !== undefined) { + localVarQueryParameter['deviceId'] = deviceId; + } + + if (checksum !== undefined) { + localVarQueryParameter['checksum'] = checksum; + } + + if (isArchived !== undefined) { + localVarQueryParameter['isArchived'] = isArchived; + } + + if (isEncoded !== undefined) { + localVarQueryParameter['isEncoded'] = isEncoded; + } + + if (isExternal !== undefined) { + localVarQueryParameter['isExternal'] = isExternal; + } + + if (isFavorite !== undefined) { + localVarQueryParameter['isFavorite'] = isFavorite; + } + + if (isMotion !== undefined) { + localVarQueryParameter['isMotion'] = isMotion; + } + + if (isOffline !== undefined) { + localVarQueryParameter['isOffline'] = isOffline; + } + + if (isReadOnly !== undefined) { + localVarQueryParameter['isReadOnly'] = isReadOnly; + } + + if (isVisible !== undefined) { + localVarQueryParameter['isVisible'] = isVisible; + } + + if (withDeleted !== undefined) { + localVarQueryParameter['withDeleted'] = withDeleted; + } + + if (withStacked !== undefined) { + localVarQueryParameter['withStacked'] = withStacked; + } + + if (withExif !== undefined) { + localVarQueryParameter['withExif'] = withExif; + } + + if (withPeople !== undefined) { + localVarQueryParameter['withPeople'] = withPeople; + } + + if (createdBefore !== undefined) { + localVarQueryParameter['createdBefore'] = (createdBefore as any instanceof Date) ? + (createdBefore as any).toISOString() : + createdBefore; + } + + if (createdAfter !== undefined) { + localVarQueryParameter['createdAfter'] = (createdAfter as any instanceof Date) ? + (createdAfter as any).toISOString() : + createdAfter; + } + + if (updatedBefore !== undefined) { + localVarQueryParameter['updatedBefore'] = (updatedBefore as any instanceof Date) ? + (updatedBefore as any).toISOString() : + updatedBefore; + } + + if (updatedAfter !== undefined) { + localVarQueryParameter['updatedAfter'] = (updatedAfter as any instanceof Date) ? + (updatedAfter as any).toISOString() : + updatedAfter; + } + + if (trashedBefore !== undefined) { + localVarQueryParameter['trashedBefore'] = (trashedBefore as any instanceof Date) ? + (trashedBefore as any).toISOString() : + trashedBefore; + } + + if (trashedAfter !== undefined) { + localVarQueryParameter['trashedAfter'] = (trashedAfter as any instanceof Date) ? + (trashedAfter as any).toISOString() : + trashedAfter; + } + + if (takenBefore !== undefined) { + localVarQueryParameter['takenBefore'] = (takenBefore as any instanceof Date) ? + (takenBefore as any).toISOString() : + takenBefore; + } + + if (takenAfter !== undefined) { + localVarQueryParameter['takenAfter'] = (takenAfter as any instanceof Date) ? + (takenAfter as any).toISOString() : + takenAfter; + } + + if (originalFileName !== undefined) { + localVarQueryParameter['originalFileName'] = originalFileName; + } + + if (originalPath !== undefined) { + localVarQueryParameter['originalPath'] = originalPath; + } + + if (resizePath !== undefined) { + localVarQueryParameter['resizePath'] = resizePath; + } + + if (webpPath !== undefined) { + localVarQueryParameter['webpPath'] = webpPath; + } + + if (encodedVideoPath !== undefined) { + localVarQueryParameter['encodedVideoPath'] = encodedVideoPath; + } + + if (city !== undefined) { + localVarQueryParameter['city'] = city; + } + + if (state !== undefined) { + localVarQueryParameter['state'] = state; + } + + if (country !== undefined) { + localVarQueryParameter['country'] = country; + } + + if (make !== undefined) { + localVarQueryParameter['make'] = make; + } + + if (model !== undefined) { + localVarQueryParameter['model'] = model; + } + + if (lensModel !== undefined) { + localVarQueryParameter['lensModel'] = lensModel; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (size !== undefined) { + localVarQueryParameter['size'] = size; + } + - localVarHeaderParameter['Content-Type'] = 'application/json'; - setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(searchAssetDto, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -8093,6 +8367,16 @@ export const AssetApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(skip, take, userId, isFavorite, isArchived, updatedAfter, updatedBefore, ifNoneMatch, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * Get all asset of a device that are in the database, ID only. + * @param {string} deviceId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAllUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllUserAssetsByDeviceId(deviceId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * Get a single asset\'s information * @param {string} id @@ -8211,12 +8495,13 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options); + async getTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -8229,32 +8514,25 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, - /** - * Get all asset of a device that are in the database, ID only. - * @param {string} deviceId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async getUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getUserAssetsByDeviceId(deviceId, options); + async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * - * @param {ImportAssetDto} importAssetDto + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release + * @param {string} deviceId * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ - async importFile(importAssetDto: ImportAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.importFile(importAssetDto, options); + async getUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getUserAssetsByDeviceId(deviceId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -8288,12 +8566,51 @@ export const AssetApiFp = function(configuration?: Configuration) { }, /** * - * @param {SearchAssetDto} searchAssetDto + * @param {string} [id] + * @param {string} [libraryId] + * @param {AssetTypeEnum} [type] + * @param {AssetOrder} [order] + * @param {string} [deviceAssetId] + * @param {string} [deviceId] + * @param {string} [checksum] + * @param {boolean} [isArchived] + * @param {boolean} [isEncoded] + * @param {boolean} [isExternal] + * @param {boolean} [isFavorite] + * @param {boolean} [isMotion] + * @param {boolean} [isOffline] + * @param {boolean} [isReadOnly] + * @param {boolean} [isVisible] + * @param {boolean} [withDeleted] + * @param {boolean} [withStacked] + * @param {boolean} [withExif] + * @param {boolean} [withPeople] + * @param {string} [createdBefore] + * @param {string} [createdAfter] + * @param {string} [updatedBefore] + * @param {string} [updatedAfter] + * @param {string} [trashedBefore] + * @param {string} [trashedAfter] + * @param {string} [takenBefore] + * @param {string} [takenAfter] + * @param {string} [originalFileName] + * @param {string} [originalPath] + * @param {string} [resizePath] + * @param {string} [webpPath] + * @param {string} [encodedVideoPath] + * @param {string} [city] + * @param {string} [state] + * @param {string} [country] + * @param {string} [make] + * @param {string} [model] + * @param {string} [lensModel] + * @param {number} [page] + * @param {number} [size] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async searchAsset(searchAssetDto: SearchAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.searchAsset(searchAssetDto, options); + async searchAssets(id?: string, libraryId?: string, type?: AssetTypeEnum, order?: AssetOrder, deviceAssetId?: string, deviceId?: string, checksum?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, withDeleted?: boolean, withStacked?: boolean, withExif?: boolean, withPeople?: boolean, createdBefore?: string, createdAfter?: string, updatedBefore?: string, updatedAfter?: string, trashedBefore?: string, trashedAfter?: string, takenBefore?: string, takenAfter?: string, originalFileName?: string, originalPath?: string, resizePath?: string, webpPath?: string, encodedVideoPath?: string, city?: string, state?: string, country?: string, make?: string, model?: string, lensModel?: string, page?: number, size?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.searchAssets(id, libraryId, type, order, deviceAssetId, deviceId, checksum, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, withDeleted, withStacked, withExif, withPeople, createdBefore, createdAfter, updatedBefore, updatedAfter, trashedBefore, trashedAfter, takenBefore, takenAfter, originalFileName, originalPath, resizePath, webpPath, encodedVideoPath, city, state, country, make, model, lensModel, page, size, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -8437,6 +8754,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath)); }, + /** + * Get all asset of a device that are in the database, ID only. + * @param {AssetApiGetAllUserAssetsByDeviceIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllUserAssetsByDeviceId(requestParameters: AssetApiGetAllUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(axios, basePath)); + }, /** * Get a single asset\'s information * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. @@ -8531,7 +8857,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath)); + return localVarFp.getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** * @@ -8540,26 +8866,19 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath)); + return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ getUserAssetsByDeviceId(requestParameters: AssetApiGetUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.getUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(axios, basePath)); }, - /** - * - * @param {AssetApiImportFileRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - importFile(requestParameters: AssetApiImportFileRequest, options?: AxiosRequestConfig): AxiosPromise { - return localVarFp.importFile(requestParameters.importAssetDto, options).then((request) => request(axios, basePath)); - }, /** * * @param {AssetApiRestoreAssetsRequest} requestParameters Request parameters. @@ -8588,12 +8907,12 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath }, /** * - * @param {AssetApiSearchAssetRequest} requestParameters Request parameters. + * @param {AssetApiSearchAssetsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - searchAsset(requestParameters: AssetApiSearchAssetRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.searchAsset(requestParameters.searchAssetDto, options).then((request) => request(axios, basePath)); + searchAssets(requestParameters: AssetApiSearchAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.searchAssets(requestParameters.id, requestParameters.libraryId, requestParameters.type, requestParameters.order, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.checksum, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.withDeleted, requestParameters.withStacked, requestParameters.withExif, requestParameters.withPeople, requestParameters.createdBefore, requestParameters.createdAfter, requestParameters.updatedBefore, requestParameters.updatedAfter, requestParameters.trashedBefore, requestParameters.trashedAfter, requestParameters.takenBefore, requestParameters.takenAfter, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.resizePath, requestParameters.webpPath, requestParameters.encodedVideoPath, requestParameters.city, requestParameters.state, requestParameters.country, requestParameters.make, requestParameters.model, requestParameters.lensModel, requestParameters.page, requestParameters.size, options).then((request) => request(axios, basePath)); }, /** * @@ -8790,6 +9109,20 @@ export interface AssetApiGetAllAssetsRequest { readonly ifNoneMatch?: string } +/** + * Request parameters for getAllUserAssetsByDeviceId operation in AssetApi. + * @export + * @interface AssetApiGetAllUserAssetsByDeviceIdRequest + */ +export interface AssetApiGetAllUserAssetsByDeviceIdRequest { + /** + * + * @type {string} + * @memberof AssetApiGetAllUserAssetsByDeviceId + */ + readonly deviceId: string +} + /** * Request parameters for getAssetById operation in AssetApi. * @export @@ -9027,6 +9360,13 @@ export interface AssetApiGetTimeBucketRequest { */ readonly withStacked?: boolean + /** + * + * @type {boolean} + * @memberof AssetApiGetTimeBucket + */ + readonly withPartners?: boolean + /** * * @type {string} @@ -9097,6 +9437,13 @@ export interface AssetApiGetTimeBucketsRequest { */ readonly withStacked?: boolean + /** + * + * @type {boolean} + * @memberof AssetApiGetTimeBuckets + */ + readonly withPartners?: boolean + /** * * @type {string} @@ -9119,20 +9466,6 @@ export interface AssetApiGetUserAssetsByDeviceIdRequest { readonly deviceId: string } -/** - * Request parameters for importFile operation in AssetApi. - * @export - * @interface AssetApiImportFileRequest - */ -export interface AssetApiImportFileRequest { - /** - * - * @type {ImportAssetDto} - * @memberof AssetApiImportFile - */ - readonly importAssetDto: ImportAssetDto -} - /** * Request parameters for restoreAssets operation in AssetApi. * @export @@ -9162,17 +9495,290 @@ export interface AssetApiRunAssetJobsRequest { } /** - * Request parameters for searchAsset operation in AssetApi. + * Request parameters for searchAssets operation in AssetApi. * @export - * @interface AssetApiSearchAssetRequest + * @interface AssetApiSearchAssetsRequest */ -export interface AssetApiSearchAssetRequest { +export interface AssetApiSearchAssetsRequest { /** * - * @type {SearchAssetDto} - * @memberof AssetApiSearchAsset + * @type {string} + * @memberof AssetApiSearchAssets */ - readonly searchAssetDto: SearchAssetDto + readonly id?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly libraryId?: string + + /** + * + * @type {AssetTypeEnum} + * @memberof AssetApiSearchAssets + */ + readonly type?: AssetTypeEnum + + /** + * + * @type {AssetOrder} + * @memberof AssetApiSearchAssets + */ + readonly order?: AssetOrder + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly deviceAssetId?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly deviceId?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly checksum?: string + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isArchived?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isEncoded?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isExternal?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isFavorite?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isMotion?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isOffline?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isReadOnly?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isVisible?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly withDeleted?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly withStacked?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly withExif?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly withPeople?: boolean + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly createdBefore?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly createdAfter?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly updatedBefore?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly updatedAfter?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly trashedBefore?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly trashedAfter?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly takenBefore?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly takenAfter?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly originalFileName?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly originalPath?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly resizePath?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly webpPath?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly encodedVideoPath?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly city?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly state?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly country?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly make?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly model?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly lensModel?: string + + /** + * + * @type {number} + * @memberof AssetApiSearchAssets + */ + readonly page?: number + + /** + * + * @type {number} + * @memberof AssetApiSearchAssets + */ + readonly size?: number } /** @@ -9461,6 +10067,17 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); } + /** + * Get all asset of a device that are in the database, ID only. + * @param {AssetApiGetAllUserAssetsByDeviceIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AssetApi + */ + public getAllUserAssetsByDeviceId(requestParameters: AssetApiGetAllUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath)); + } + /** * Get a single asset\'s information * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. @@ -9576,7 +10193,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** @@ -9587,13 +10204,15 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} * @memberof AssetApi */ @@ -9601,17 +10220,6 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).getUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath)); } - /** - * - * @param {AssetApiImportFileRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public importFile(requestParameters: AssetApiImportFileRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).importFile(requestParameters.importAssetDto, options).then((request) => request(this.axios, this.basePath)); - } - /** * * @param {AssetApiRestoreAssetsRequest} requestParameters Request parameters. @@ -9646,13 +10254,13 @@ export class AssetApi extends BaseAPI { /** * - * @param {AssetApiSearchAssetRequest} requestParameters Request parameters. + * @param {AssetApiSearchAssetsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AssetApi */ - public searchAsset(requestParameters: AssetApiSearchAssetRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).searchAsset(requestParameters.searchAssetDto, options).then((request) => request(this.axios, this.basePath)); + public searchAssets(requestParameters: AssetApiSearchAssetsRequest = {}, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).searchAssets(requestParameters.id, requestParameters.libraryId, requestParameters.type, requestParameters.order, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.checksum, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.withDeleted, requestParameters.withStacked, requestParameters.withExif, requestParameters.withPeople, requestParameters.createdBefore, requestParameters.createdAfter, requestParameters.updatedBefore, requestParameters.updatedAfter, requestParameters.trashedBefore, requestParameters.trashedAfter, requestParameters.takenBefore, requestParameters.takenAfter, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.resizePath, requestParameters.webpPath, requestParameters.encodedVideoPath, requestParameters.city, requestParameters.state, requestParameters.country, requestParameters.make, requestParameters.model, requestParameters.lensModel, requestParameters.page, requestParameters.size, options).then((request) => request(this.axios, this.basePath)); } /** @@ -10497,7 +11105,7 @@ export const AuthenticationApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async signUpAdmin(signUpDto: SignUpDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async signUpAdmin(signUpDto: SignUpDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.signUpAdmin(signUpDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -10577,7 +11185,7 @@ export const AuthenticationApiFactory = function (configuration?: Configuration, * @param {*} [options] Override http request option. * @throws {RequiredError} */ - signUpAdmin(requestParameters: AuthenticationApiSignUpAdminRequest, options?: AxiosRequestConfig): AxiosPromise { + signUpAdmin(requestParameters: AuthenticationApiSignUpAdminRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.signUpAdmin(requestParameters.signUpDto, options).then((request) => request(axios, basePath)); }, /** @@ -12296,6 +12904,54 @@ export const PartnerApiAxiosParamCreator = function (configuration?: Configurati let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id + * @param {UpdatePartnerDto} updatePartnerDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartner: async (id: string, updatePartnerDto: UpdatePartnerDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('updatePartner', 'id', id) + // verify required parameter 'updatePartnerDto' is not null or undefined + assertParamExists('updatePartner', 'updatePartnerDto', updatePartnerDto) + const localVarPath = `/partner/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updatePartnerDto, localVarRequestOptions, configuration) + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -12317,7 +12973,7 @@ export const PartnerApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async createPartner(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async createPartner(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.createPartner(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -12327,7 +12983,7 @@ export const PartnerApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getPartners(direction: 'shared-by' | 'shared-with', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + async getPartners(direction: 'shared-by' | 'shared-with', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.getPartners(direction, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -12341,6 +12997,17 @@ export const PartnerApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.removePartner(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} id + * @param {UpdatePartnerDto} updatePartnerDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updatePartner(id: string, updatePartnerDto: UpdatePartnerDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updatePartner(id, updatePartnerDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -12357,7 +13024,7 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa * @param {*} [options] Override http request option. * @throws {RequiredError} */ - createPartner(requestParameters: PartnerApiCreatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { + createPartner(requestParameters: PartnerApiCreatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.createPartner(requestParameters.id, options).then((request) => request(axios, basePath)); }, /** @@ -12366,7 +13033,7 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getPartners(requestParameters: PartnerApiGetPartnersRequest, options?: AxiosRequestConfig): AxiosPromise> { + getPartners(requestParameters: PartnerApiGetPartnersRequest, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.getPartners(requestParameters.direction, options).then((request) => request(axios, basePath)); }, /** @@ -12378,6 +13045,15 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.removePartner(requestParameters.id, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {PartnerApiUpdatePartnerRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartner(requestParameters: PartnerApiUpdatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.updatePartner(requestParameters.id, requestParameters.updatePartnerDto, options).then((request) => request(axios, basePath)); + }, }; }; @@ -12423,6 +13099,27 @@ export interface PartnerApiRemovePartnerRequest { readonly id: string } +/** + * Request parameters for updatePartner operation in PartnerApi. + * @export + * @interface PartnerApiUpdatePartnerRequest + */ +export interface PartnerApiUpdatePartnerRequest { + /** + * + * @type {string} + * @memberof PartnerApiUpdatePartner + */ + readonly id: string + + /** + * + * @type {UpdatePartnerDto} + * @memberof PartnerApiUpdatePartner + */ + readonly updatePartnerDto: UpdatePartnerDto +} + /** * PartnerApi - object-oriented interface * @export @@ -12462,6 +13159,17 @@ export class PartnerApi extends BaseAPI { public removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig) { return PartnerApiFp(this.configuration).removePartner(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @param {PartnerApiUpdatePartnerRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PartnerApi + */ + public updatePartner(requestParameters: PartnerApiUpdatePartnerRequest, options?: AxiosRequestConfig) { + return PartnerApiFp(this.configuration).updatePartner(requestParameters.id, requestParameters.updatePartnerDto, options).then((request) => request(this.axios, this.basePath)); + } } @@ -15088,6 +15796,51 @@ export const SystemConfigApiAxiosParamCreator = function (configuration?: Config + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {MapTheme} theme + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMapStyle: async (theme: MapTheme, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'theme' is not null or undefined + assertParamExists('getMapStyle', 'theme', theme) + const localVarPath = `/system-config/map/style.json`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (theme !== undefined) { + localVarQueryParameter['theme'] = theme; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -15207,6 +15960,16 @@ export const SystemConfigApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getConfigDefaults(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {MapTheme} theme + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getMapStyle(theme: MapTheme, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMapStyle(theme, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {*} [options] Override http request option. @@ -15252,6 +16015,15 @@ export const SystemConfigApiFactory = function (configuration?: Configuration, b getConfigDefaults(options?: AxiosRequestConfig): AxiosPromise { return localVarFp.getConfigDefaults(options).then((request) => request(axios, basePath)); }, + /** + * + * @param {SystemConfigApiGetMapStyleRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMapStyle(requestParameters: SystemConfigApiGetMapStyleRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.getMapStyle(requestParameters.theme, options).then((request) => request(axios, basePath)); + }, /** * * @param {*} [options] Override http request option. @@ -15272,6 +16044,20 @@ export const SystemConfigApiFactory = function (configuration?: Configuration, b }; }; +/** + * Request parameters for getMapStyle operation in SystemConfigApi. + * @export + * @interface SystemConfigApiGetMapStyleRequest + */ +export interface SystemConfigApiGetMapStyleRequest { + /** + * + * @type {MapTheme} + * @memberof SystemConfigApiGetMapStyle + */ + readonly theme: MapTheme +} + /** * Request parameters for updateConfig operation in SystemConfigApi. * @export @@ -15313,6 +16099,17 @@ export class SystemConfigApi extends BaseAPI { return SystemConfigApiFp(this.configuration).getConfigDefaults(options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {SystemConfigApiGetMapStyleRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SystemConfigApi + */ + public getMapStyle(requestParameters: SystemConfigApiGetMapStyleRequest, options?: AxiosRequestConfig) { + return SystemConfigApiFp(this.configuration).getMapStyle(requestParameters.theme, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {*} [options] Override http request option. @@ -16184,6 +16981,44 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration) options: localVarRequestOptions, }; }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteProfileImage: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/user/profile-image`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {string} id @@ -16509,6 +17344,15 @@ export const UserApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteProfileImage(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteProfileImage(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {string} id @@ -16606,6 +17450,14 @@ export const UserApiFactory = function (configuration?: Configuration, basePath? createUser(requestParameters: UserApiCreateUserRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.createUser(requestParameters.createUserDto, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteProfileImage(options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.deleteProfileImage(options).then((request) => request(axios, basePath)); + }, /** * * @param {UserApiDeleteUserRequest} requestParameters Request parameters. @@ -16812,6 +17664,16 @@ export class UserApi extends BaseAPI { return UserApiFp(this.configuration).createUser(requestParameters.createUserDto, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UserApi + */ + public deleteProfileImage(options?: AxiosRequestConfig) { + return UserApiFp(this.configuration).deleteProfileImage(options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {UserApiDeleteUserRequest} requestParameters Request parameters. diff --git a/cli/src/api/open-api/base.ts b/cli/src/api/open-api/base.ts index 9a534e7bd..f16265aff 100644 --- a/cli/src/api/open-api/base.ts +++ b/cli/src/api/open-api/base.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.84.0 + * The version of the OpenAPI document: 1.89.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/cli/src/api/open-api/common.ts b/cli/src/api/open-api/common.ts index 8997b2d52..c48b923a0 100644 --- a/cli/src/api/open-api/common.ts +++ b/cli/src/api/open-api/common.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.84.0 + * The version of the OpenAPI document: 1.89.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/cli/src/api/open-api/configuration.ts b/cli/src/api/open-api/configuration.ts index 8058881d1..030a53c76 100644 --- a/cli/src/api/open-api/configuration.ts +++ b/cli/src/api/open-api/configuration.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.84.0 + * The version of the OpenAPI document: 1.89.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/cli/src/api/open-api/index.ts b/cli/src/api/open-api/index.ts index d0651f28a..19d849d0c 100644 --- a/cli/src/api/open-api/index.ts +++ b/cli/src/api/open-api/index.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.84.0 + * The version of the OpenAPI document: 1.89.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/cli/src/cli/base-command.ts b/cli/src/cli/base-command.ts index c2fb8fee9..d47f973ac 100644 --- a/cli/src/cli/base-command.ts +++ b/cli/src/cli/base-command.ts @@ -9,7 +9,6 @@ import { ServerVersionResponseDto, UserResponseDto } from 'src/api/open-api'; export abstract class BaseCommand { protected sessionService!: SessionService; protected immichApi!: ImmichApi; - protected deviceId!: string; protected user!: UserResponseDto; protected serverVersion!: ServerVersionResponseDto; diff --git a/cli/src/commands/server-info.ts b/cli/src/commands/server-info.ts index 5024604a1..ec101ac04 100644 --- a/cli/src/commands/server-info.ts +++ b/cli/src/commands/server-info.ts @@ -1,15 +1,19 @@ import { BaseCommand } from '../cli/base-command'; export default class ServerInfo extends BaseCommand { - static description = 'Display server information'; - static enableJsonFlag = true; - public async run() { - console.log('Getting server information'); - await this.connect(); const { data: versionInfo } = await this.immichApi.serverInfoApi.getServerVersion(); - console.log(versionInfo); + console.log(`Server is running version ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`); + + const { data: supportedmedia } = await this.immichApi.serverInfoApi.getSupportedMediaTypes(); + + console.log(`Supported image types: ${supportedmedia.image.map((extension) => extension.replace('.', ''))}`); + + console.log(`Supported video types: ${supportedmedia.video.map((extension) => extension.replace('.', ''))}`); + + const { data: statistics } = await this.immichApi.assetApi.getAssetStatistics(); + console.log(`Images: ${statistics.images}, Videos: ${statistics.videos}, Total: ${statistics.total}`); } } diff --git a/cli/src/commands/upload.ts b/cli/src/commands/upload.ts index 93cb36ec2..8bd3d24d9 100644 --- a/cli/src/commands/upload.ts +++ b/cli/src/commands/upload.ts @@ -1,43 +1,38 @@ -import { BaseCommand } from '../cli/base-command'; -import { CrawledAsset } from '../cores/models/crawled-asset'; -import { CrawlService, UploadService } from '../services'; -import * as si from 'systeminformation'; -import FormData from 'form-data'; +import { Asset } from '../cores/models/asset'; +import { CrawlService } from '../services'; import { UploadOptionsDto } from '../cores/dto/upload-options-dto'; import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto'; import cliProgress from 'cli-progress'; import byteSize from 'byte-size'; +import { BaseCommand } from '../cli/base-command'; +import axios, { AxiosRequestConfig } from 'axios'; +import FormData from 'form-data'; export default class Upload extends BaseCommand { - private crawlService = new CrawlService(); - private uploadService!: UploadService; - deviceId!: string; uploadLength!: number; - dryRun = false; public async run(paths: string[], options: UploadOptionsDto): Promise { await this.connect(); - const uuid = await si.uuid(); - this.deviceId = uuid.os || 'CLI'; - this.uploadService = new UploadService(this.immichApi.apiConfiguration); + const deviceId = 'CLI'; - this.dryRun = options.dryRun; + const formatResponse = await this.immichApi.serverInfoApi.getSupportedMediaTypes(); + const crawlService = new CrawlService(formatResponse.data.image, formatResponse.data.video); const crawlOptions = new CrawlOptionsDto(); crawlOptions.pathsToCrawl = paths; crawlOptions.recursive = options.recursive; - crawlOptions.excludePatterns = options.excludePatterns; + crawlOptions.exclusionPatterns = options.exclusionPatterns; - const crawledFiles: string[] = await this.crawlService.crawl(crawlOptions); + const crawledFiles: string[] = await crawlService.crawl(crawlOptions); if (crawledFiles.length === 0) { console.log('No assets found, exiting'); return; } - const assetsToUpload = crawledFiles.map((path) => new CrawledAsset(path)); + const assetsToUpload = crawledFiles.map((path) => new Asset(path, deviceId)); const uploadProgress = new cliProgress.SingleBar( { @@ -58,118 +53,108 @@ export default class Upload extends BaseCommand { totalSize += asset.fileSize; } + const existingAlbums = (await this.immichApi.albumApi.getAllAlbums()).data; + uploadProgress.start(totalSize, 0); uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) }); - for (const asset of assetsToUpload) { - uploadProgress.update({ - filename: asset.path, - }); + try { + for (const asset of assetsToUpload) { + uploadProgress.update({ + filename: asset.path, + }); - try { - if (options.import) { - const importData = { - assetPath: asset.path, - sidecarPath: asset.sidecarPath, - deviceAssetId: asset.deviceAssetId, - deviceId: this.deviceId, - fileCreatedAt: asset.fileCreatedAt, - fileModifiedAt: asset.fileModifiedAt, - isFavorite: false, - isReadOnly: options.readOnly, - }; + let skipUpload = false; + if (!options.skipHash) { + const assetBulkUploadCheckDto = { assets: [{ id: asset.path, checksum: await asset.hash() }] }; - if (!this.dryRun) { - await this.uploadService.import(importData); - } - } else { - await this.uploadAsset(asset, options.skipHash); + const checkResponse = await this.immichApi.assetApi.checkBulkUpload({ + assetBulkUploadCheckDto, + }); + + skipUpload = checkResponse.data.results[0].action === 'reject'; } - } catch (error) { - uploadProgress.stop(); - throw error; - } - sizeSoFar += asset.fileSize; - if (!asset.skipped) { - totalSizeUploaded += asset.fileSize; - uploadCounter++; - } + if (!skipUpload) { + if (!options.dryRun) { + const formData = asset.getUploadFormData(); + const res = await this.uploadAsset(formData); - uploadProgress.update(sizeSoFar, { value_formatted: byteSize(sizeSoFar) }); + if (options.album && asset.albumName) { + let album = existingAlbums.find((album) => album.albumName === asset.albumName); + if (!album) { + const res = await this.immichApi.albumApi.createAlbum({ + createAlbumDto: { albumName: asset.albumName }, + }); + album = res.data; + existingAlbums.push(album); + } + + await this.immichApi.albumApi.addAssetsToAlbum({ id: album.id, bulkIdsDto: { ids: [res.data.id] } }); + } + } + + totalSizeUploaded += asset.fileSize; + uploadCounter++; + } + + sizeSoFar += asset.fileSize; + + uploadProgress.update(sizeSoFar, { value_formatted: byteSize(sizeSoFar) }); + } + } finally { + uploadProgress.stop(); } - uploadProgress.stop(); - let messageStart; - if (this.dryRun) { - messageStart = 'Would have '; + if (options.dryRun) { + messageStart = 'Would have'; } else { - messageStart = 'Successfully '; + messageStart = 'Successfully'; } - if (options.import) { - console.log(`${messageStart} imported ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`); + if (uploadCounter === 0) { + console.log('All assets were already uploaded, nothing to do.'); } else { - if (uploadCounter === 0) { - console.log('All assets were already uploaded, nothing to do.'); + console.log(`${messageStart} uploaded ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`); + } + if (options.delete) { + if (options.dryRun) { + console.log(`Would now have deleted assets, but skipped due to dry run`); } else { - console.log(`${messageStart} uploaded ${uploadCounter} assets (${byteSize(totalSizeUploaded)})`); - } - if (options.delete) { - if (this.dryRun) { - console.log(`Would now have deleted assets, but skipped due to dry run`); - } else { - console.log('Deleting assets that have been uploaded...'); - const deletionProgress = new cliProgress.SingleBar(cliProgress.Presets.shades_classic); - deletionProgress.start(crawledFiles.length, 0); + console.log('Deleting assets that have been uploaded...'); + const deletionProgress = new cliProgress.SingleBar(cliProgress.Presets.shades_classic); + deletionProgress.start(crawledFiles.length, 0); - for (const asset of assetsToUpload) { - if (!this.dryRun) { - await asset.delete(); - } - deletionProgress.increment(); + for (const asset of assetsToUpload) { + if (!options.dryRun) { + await asset.delete(); } - deletionProgress.stop(); - console.log('Deletion complete'); + deletionProgress.increment(); } + deletionProgress.stop(); + console.log('Deletion complete'); } } } - private async uploadAsset(asset: CrawledAsset, skipHash = false) { - await asset.readData(); + private async uploadAsset(data: FormData): Promise { + const url = this.immichApi.apiConfiguration.instanceUrl + '/asset/upload'; - let skipUpload = false; - if (!skipHash) { - const checksum = await asset.hash(); + const config: AxiosRequestConfig = { + method: 'post', + maxRedirects: 0, + url, + headers: { + 'x-api-key': this.immichApi.apiConfiguration.apiKey, + ...data.getHeaders(), + }, + maxContentLength: Infinity, + maxBodyLength: Infinity, + data, + }; - const checkResponse = await this.uploadService.checkIfAssetAlreadyExists(asset.path, checksum); - skipUpload = checkResponse.data.results[0].action === 'reject'; - } - - if (skipUpload) { - asset.skipped = true; - } else { - const uploadFormData = new FormData(); - - uploadFormData.append('deviceAssetId', asset.deviceAssetId); - uploadFormData.append('deviceId', this.deviceId); - uploadFormData.append('fileCreatedAt', asset.fileCreatedAt); - uploadFormData.append('fileModifiedAt', asset.fileModifiedAt); - uploadFormData.append('isFavorite', String(false)); - uploadFormData.append('assetData', asset.assetData, { filename: asset.path }); - - if (asset.sidecarData) { - uploadFormData.append('sidecarData', asset.sidecarData, { - filename: asset.sidecarPath, - contentType: 'application/xml', - }); - } - - if (!this.dryRun) { - await this.uploadService.upload(uploadFormData); - } - } + const res = await axios(config); + return res; } } diff --git a/cli/src/cores/constants.ts b/cli/src/cores/constants.ts deleted file mode 100644 index fd9659efd..000000000 --- a/cli/src/cores/constants.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Check asset-upload.config.spec.ts for complete list -// TODO: we should get this list from the server via API in the future - -// Videos -const videos = ['mp4', 'webm', 'mov', '3gp', 'avi', 'm2ts', 'mts', 'mpg', 'flv', 'mkv', 'wmv']; - -// Images -const heic = ['heic', 'heif']; -const jpeg = ['jpg', 'jpeg']; -const png = ['png']; -const gif = ['gif']; -const tiff = ['tif', 'tiff']; -const webp = ['webp']; -const dng = ['dng']; -const other = [ - '3fr', - 'ari', - 'arw', - 'avif', - 'cap', - 'cin', - 'cr2', - 'cr3', - 'crw', - 'dcr', - 'nef', - 'erf', - 'fff', - 'iiq', - 'jxl', - 'k25', - 'kdc', - 'mrw', - 'orf', - 'ori', - 'pef', - 'psd', - 'raf', - 'raw', - 'rwl', - 'sr2', - 'srf', - 'srw', - 'orf', - 'ori', - 'x3f', -]; - -export const ACCEPTED_FILE_EXTENSIONS = [ - ...videos, - ...jpeg, - ...png, - ...heic, - ...gif, - ...tiff, - ...webp, - ...dng, - ...other, -]; diff --git a/cli/src/cores/dto/crawl-options-dto.ts b/cli/src/cores/dto/crawl-options-dto.ts index f9435d390..bf80b4639 100644 --- a/cli/src/cores/dto/crawl-options-dto.ts +++ b/cli/src/cores/dto/crawl-options-dto.ts @@ -1,6 +1,6 @@ export class CrawlOptionsDto { pathsToCrawl!: string[]; - recursive = false; - includeHidden = false; - excludePatterns!: string[]; + recursive? = false; + includeHidden? = false; + exclusionPatterns?: string[]; } diff --git a/cli/src/cores/dto/upload-options-dto.ts b/cli/src/cores/dto/upload-options-dto.ts index 62538cc15..b788f9b4f 100644 --- a/cli/src/cores/dto/upload-options-dto.ts +++ b/cli/src/cores/dto/upload-options-dto.ts @@ -1,9 +1,9 @@ export class UploadOptionsDto { recursive = false; - excludePatterns!: string[]; + exclusionPatterns!: string[]; dryRun = false; skipHash = false; delete = false; - import = false; readOnly = true; + album = false; } diff --git a/cli/src/cores/index.ts b/cli/src/cores/index.ts index 11d6a4170..e9644dae4 100644 --- a/cli/src/cores/index.ts +++ b/cli/src/cores/index.ts @@ -1,2 +1 @@ -export * from './constants'; export * from './models'; diff --git a/cli/src/cores/models/asset.ts b/cli/src/cores/models/asset.ts new file mode 100644 index 000000000..78f7ddba7 --- /dev/null +++ b/cli/src/cores/models/asset.ts @@ -0,0 +1,100 @@ +import * as fs from 'fs'; +import { basename } from 'node:path'; +import crypto from 'crypto'; +import Os from 'os'; +import FormData from 'form-data'; + +export class Asset { + readonly path: string; + readonly deviceId!: string; + + assetData?: fs.ReadStream; + deviceAssetId?: string; + fileCreatedAt?: string; + fileModifiedAt?: string; + sidecarData?: fs.ReadStream; + sidecarPath?: string; + fileSize!: number; + albumName?: string; + + constructor(path: string, deviceId: string) { + this.path = path; + this.deviceId = deviceId; + } + + async process() { + const stats = await fs.promises.stat(this.path); + this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, ''); + this.fileCreatedAt = stats.mtime.toISOString(); + this.fileModifiedAt = stats.mtime.toISOString(); + this.fileSize = stats.size; + this.albumName = this.extractAlbumName(); + + this.assetData = this.getReadStream(this.path); + + // TODO: doesn't xmp replace the file extension? Will need investigation + const sideCarPath = `${this.path}.xmp`; + try { + fs.accessSync(sideCarPath, fs.constants.R_OK); + this.sidecarData = this.getReadStream(sideCarPath); + } catch (error) {} + } + + getUploadFormData(): FormData { + if (!this.assetData) throw new Error('Asset data not set'); + if (!this.deviceAssetId) throw new Error('Device asset id not set'); + if (!this.fileCreatedAt) throw new Error('File created at not set'); + if (!this.fileModifiedAt) throw new Error('File modified at not set'); + if (!this.deviceId) throw new Error('Device id not set'); + + const data: any = { + assetData: this.assetData as any, + deviceAssetId: this.deviceAssetId, + deviceId: this.deviceId, + fileCreatedAt: this.fileCreatedAt, + fileModifiedAt: this.fileModifiedAt, + isFavorite: String(false), + }; + const formData = new FormData(); + + for (const prop in data) { + formData.append(prop, data[prop]); + } + + if (this.sidecarData) { + formData.append('sidecarData', this.sidecarData); + } + + return formData; + } + + private getReadStream(path: string): fs.ReadStream { + return fs.createReadStream(path); + } + + async delete(): Promise { + return fs.promises.unlink(this.path); + } + + public async hash(): Promise { + const sha1 = (filePath: string) => { + const hash = crypto.createHash('sha1'); + return new Promise((resolve, reject) => { + const rs = fs.createReadStream(filePath); + rs.on('error', reject); + rs.on('data', (chunk) => hash.update(chunk)); + rs.on('end', () => resolve(hash.digest('hex'))); + }); + }; + + return await sha1(this.path); + } + + private extractAlbumName(): string { + if (Os.platform() === 'win32') { + return this.path.split('\\').slice(-2)[0]; + } else { + return this.path.split('/').slice(-2)[0]; + } + } +} diff --git a/cli/src/cores/models/crawled-asset.ts b/cli/src/cores/models/crawled-asset.ts deleted file mode 100644 index 15eb72f2b..000000000 --- a/cli/src/cores/models/crawled-asset.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as fs from 'fs'; -import { basename } from 'node:path'; -import crypto from 'crypto'; - -export class CrawledAsset { - public path: string; - - public assetData?: fs.ReadStream; - public deviceAssetId?: string; - public fileCreatedAt?: string; - public fileModifiedAt?: string; - public sidecarData?: Buffer; - public sidecarPath?: string; - public fileSize!: number; - public skipped = false; - - constructor(path: string) { - this.path = path; - } - - async readData() { - this.assetData = fs.createReadStream(this.path); - } - - async process() { - const stats = await fs.promises.stat(this.path); - this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, ''); - this.fileCreatedAt = stats.mtime.toISOString(); - this.fileModifiedAt = stats.mtime.toISOString(); - this.fileSize = stats.size; - - // TODO: doesn't xmp replace the file extension? Will need investigation - const sideCarPath = `${this.path}.xmp`; - try { - fs.accessSync(sideCarPath, fs.constants.R_OK); - this.sidecarData = await fs.promises.readFile(sideCarPath); - this.sidecarPath = sideCarPath; - } catch (error) {} - } - - async delete(): Promise { - return fs.promises.unlink(this.path); - } - - public async hash(): Promise { - const sha1 = (filePath: string) => { - const hash = crypto.createHash('sha1'); - return new Promise((resolve, reject) => { - const rs = fs.createReadStream(filePath); - rs.on('error', reject); - rs.on('data', (chunk) => hash.update(chunk)); - rs.on('end', () => resolve(hash.digest('hex'))); - }); - }; - - return await sha1(this.path); - } -} diff --git a/cli/src/cores/models/index.ts b/cli/src/cores/models/index.ts index ae0990ceb..ea2719dd8 100644 --- a/cli/src/cores/models/index.ts +++ b/cli/src/cores/models/index.ts @@ -1 +1 @@ -export * from './crawled-asset'; +export * from './asset'; diff --git a/cli/src/index.ts b/cli/src/index.ts index c0bbfe0b3..39c17cafd 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -1,9 +1,13 @@ +#! /usr/bin/env node + import { program, Option } from 'commander'; import Upload from './commands/upload'; import ServerInfo from './commands/server-info'; import LoginKey from './commands/login/key'; +import Logout from './commands/logout'; +import { version } from '../package.json'; -program.name('immich').description('Immich command line interface'); +program.name('immich').description('Immich command line interface').version(version); program .command('upload') @@ -12,6 +16,11 @@ program .addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false)) .addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS')) .addOption(new Option('-h, --skip-hash', "Don't hash files before upload").env('IMMICH_SKIP_HASH').default(false)) + .addOption( + new Option('-a, --album', 'Automatically create albums based on folder name') + .env('IMMICH_AUTO_CREATE_ALBUM') + .default(false), + ) .addOption( new Option('-n, --dry-run', "Don't perform any actions, just show what will be done") .env('IMMICH_DRY_RUN') @@ -20,33 +29,13 @@ program .addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS')) .argument('[paths...]', 'One or more paths to assets to be uploaded') .action(async (paths, options) => { - options.excludePatterns = options.ignore; - await new Upload().run(paths, options); - }); - -program - .command('import') - .description('Import existing assets') - .usage('[options] [paths...]') - .addOption(new Option('-r, --recursive', 'Recursive').env('IMMICH_RECURSIVE').default(false)) - .addOption( - new Option('-n, --dry-run', "Don't perform any actions, just show what will be done") - .env('IMMICH_DRY_RUN') - .default(false), - ) - .addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS').default(false)) - .addOption(new Option('--no-read-only', 'Import files without read-only protection, allowing Immich to manage them')) - .argument('[paths...]', 'One or more paths to assets to be imported') - .action(async (paths, options) => { - options.import = true; - options.excludePatterns = options.ignore; + options.exclusionPatterns = options.ignore; await new Upload().run(paths, options); }); program .command('server-info') .description('Display server information') - .action(async () => { await new ServerInfo().run(); }); @@ -60,4 +49,11 @@ program await new LoginKey().run(paths, options); }); +program + .command('logout') + .description('Remove stored credentials') + .action(async () => { + await new Logout().run(); + }); + program.parse(process.argv); diff --git a/cli/src/services/crawl.service.spec.ts b/cli/src/services/crawl.service.spec.ts index 487b703e8..3957f193a 100644 --- a/cli/src/services/crawl.service.spec.ts +++ b/cli/src/services/crawl.service.spec.ts @@ -1,235 +1,206 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { CrawlService } from './crawl.service'; import mockfs from 'mock-fs'; -import { toIncludeSameMembers } from 'jest-extended'; -import { CrawlOptionsDto } from '../cores/dto/crawl-options-dto'; +import { CrawlOptionsDto } from 'src/cores/dto/crawl-options-dto'; +import { CrawlService } from '.'; -const matchers = require('jest-extended'); -expect.extend(matchers); +interface Test { + test: string; + options: CrawlOptionsDto; + files: Record; +} -const crawlService = new CrawlService(); +const cwd = process.cwd(); -describe('CrawlService', () => { - beforeAll(() => { - // Write a dummy output before mock-fs to prevent some annoying errors - console.log(); - }); +const tests: Test[] = [ + { + test: 'should return empty when crawling an empty path list', + options: { + pathsToCrawl: [], + }, + files: {}, + }, + { + test: 'should crawl a single path', + options: { + pathsToCrawl: ['/photos/'], + }, + files: { + '/photos/image.jpg': true, + }, + }, + { + test: 'should exclude by file extension', + options: { + pathsToCrawl: ['/photos/'], + exclusionPatterns: ['**/*.tif'], + }, + files: { + '/photos/image.jpg': true, + '/photos/image.tif': false, + }, + }, + { + test: 'should exclude by file extension without case sensitivity', + options: { + pathsToCrawl: ['/photos/'], + exclusionPatterns: ['**/*.TIF'], + }, + files: { + '/photos/image.jpg': true, + '/photos/image.tif': false, + }, + }, + { + test: 'should exclude by folder', + options: { + pathsToCrawl: ['/photos/'], + exclusionPatterns: ['**/raw/**'], + }, + files: { + '/photos/image.jpg': true, + '/photos/raw/image.jpg': false, + '/photos/raw2/image.jpg': true, + '/photos/folder/raw/image.jpg': false, + '/photos/crawl/image.jpg': true, + }, + }, + { + test: 'should crawl multiple paths', + options: { + pathsToCrawl: ['/photos/', '/images/', '/albums/'], + }, + files: { + '/photos/image1.jpg': true, + '/images/image2.jpg': true, + '/albums/image3.jpg': true, + }, + }, + { + test: 'should support globbing paths', + options: { + pathsToCrawl: ['/photos*'], + }, + files: { + '/photos1/image1.jpg': true, + '/photos2/image2.jpg': true, + '/images/image3.jpg': false, + }, + }, + { + test: 'should crawl a single path without trailing slash', + options: { + pathsToCrawl: ['/photos'], + }, + files: { + '/photos/image.jpg': true, + }, + }, + { + test: 'should crawl a single path', + options: { + pathsToCrawl: ['/photos/'], + }, + files: { + '/photos/image.jpg': true, + '/photos/subfolder/image1.jpg': true, + '/photos/subfolder/image2.jpg': true, + '/image1.jpg': false, + }, + }, + { + test: 'should filter file extensions', + options: { + pathsToCrawl: ['/photos/'], + }, + files: { + '/photos/image.jpg': true, + '/photos/image.txt': false, + '/photos/1': false, + }, + }, + { + test: 'should include photo and video extensions', + options: { + pathsToCrawl: ['/photos/', '/videos/'], + }, + files: { + '/photos/image.jpg': true, + '/photos/image.jpeg': true, + '/photos/image.heic': true, + '/photos/image.heif': true, + '/photos/image.png': true, + '/photos/image.gif': true, + '/photos/image.tif': true, + '/photos/image.tiff': true, + '/photos/image.webp': true, + '/photos/image.dng': true, + '/photos/image.nef': true, + '/videos/video.mp4': true, + '/videos/video.mov': true, + '/videos/video.webm': true, + }, + }, + { + test: 'should check file extensions without case sensitivity', + options: { + pathsToCrawl: ['/photos/'], + }, + files: { + '/photos/image.jpg': true, + '/photos/image.Jpg': true, + '/photos/image.jpG': true, + '/photos/image.JPG': true, + '/photos/image.jpEg': true, + '/photos/image.TIFF': true, + '/photos/image.tif': true, + '/photos/image.dng': true, + '/photos/image.NEF': true, + }, + }, + { + test: 'should normalize the path', + options: { + pathsToCrawl: ['/photos/1/../2'], + }, + files: { + '/photos/1/image.jpg': false, + '/photos/2/image.jpg': true, + }, + }, + { + test: 'should return absolute paths', + options: { + pathsToCrawl: ['photos'], + }, + files: { + [`${cwd}/photos/1.jpg`]: true, + [`${cwd}/photos/2.jpg`]: true, + [`/photos/3.jpg`]: false, + }, + }, +]; - it('should crawl a single directory', async () => { - mockfs({ - '/photos/image.jpg': '', - }); - - const options = new CrawlOptionsDto(); - options.pathsToCrawl = ['/photos/']; - const paths: string[] = await crawlService.crawl(options); - expect(paths).toIncludeSameMembers(['/photos/image.jpg']); - }); - - it('should crawl a single file', async () => { - mockfs({ - '/photos/image.jpg': '', - }); - - const options = new CrawlOptionsDto(); - options.pathsToCrawl = ['/photos/image.jpg']; - const paths: string[] = await crawlService.crawl(options); - expect(paths).toIncludeSameMembers(['/photos/image.jpg']); - }); - - it('should crawl a file and a directory', async () => { - mockfs({ - '/photos/image.jpg': '', - '/images/photo.jpg': '', - }); - - const options = new CrawlOptionsDto(); - options.pathsToCrawl = ['/photos/image.jpg', '/images/']; - const paths: string[] = await crawlService.crawl(options); - expect(paths).toIncludeSameMembers(['/photos/image.jpg', '/images/photo.jpg']); - }); - - it('should exclude by file extension', async () => { - mockfs({ - '/photos/image.jpg': '', - '/photos/image.tif': '', - }); - - const options = new CrawlOptionsDto(); - options.pathsToCrawl = ['/photos/']; - options.excludePatterns = ['**/*.tif']; - const paths: string[] = await crawlService.crawl(options); - expect(paths).toIncludeSameMembers(['/photos/image.jpg']); - }); - - it('should exclude by file extension without case sensitivity', async () => { - mockfs({ - '/photos/image.jpg': '', - '/photos/image.tif': '', - }); - - const options = new CrawlOptionsDto(); - options.pathsToCrawl = ['/photos/']; - options.excludePatterns = ['**/*.TIF']; - const paths: string[] = await crawlService.crawl(options); - expect(paths).toIncludeSameMembers(['/photos/image.jpg']); - }); - - it('should exclude by folder', async () => { - mockfs({ - '/photos/image.jpg': '', - '/photos/raw/image.jpg': '', - '/photos/raw2/image.jpg': '', - '/photos/folder/raw/image.jpg': '', - '/photos/crawl/image.jpg': '', - }); - - const options = new CrawlOptionsDto(); - options.pathsToCrawl = ['/photos/']; - options.excludePatterns = ['**/raw/**']; - options.recursive = true; - const paths: string[] = await crawlService.crawl(options); - expect(paths).toIncludeSameMembers(['/photos/image.jpg', '/photos/raw2/image.jpg', '/photos/crawl/image.jpg']); - }); - - it('should crawl multiple paths', async () => { - mockfs({ - '/photos/image1.jpg': '', - '/images/image2.jpg': '', - '/albums/image3.jpg': '', - }); - const options = new CrawlOptionsDto(); - options.pathsToCrawl = ['/photos/', '/images/', '/albums/']; - options.recursive = false; - const paths: string[] = await crawlService.crawl(options); - expect(paths).toIncludeSameMembers(['/photos/image1.jpg', '/images/image2.jpg', '/albums/image3.jpg']); - }); - - it('should crawl a single path without trailing slash', async () => { - mockfs({ - '/photos/image.jpg': '', - }); - const options = new CrawlOptionsDto(); - options.pathsToCrawl = ['/photos']; - const paths: string[] = await crawlService.crawl(options); - expect(paths).toIncludeSameMembers(['/photos/image.jpg']); - }); - - it('should crawl a single path without recursion', async () => { - mockfs({ - '/photos/image.jpg': '', - '/photos/subfolder/image1.jpg': '', - '/photos/subfolder/image2.jpg': '', - '/image1.jpg': '', - }); - - const options = new CrawlOptionsDto(); - options.pathsToCrawl = ['/photos/']; - const paths: string[] = await crawlService.crawl(options); - expect(paths).toIncludeSameMembers(['/photos/image.jpg']); - }); - - it('should crawl a single path with recursion', async () => { - mockfs({ - '/photos/image.jpg': '', - '/photos/subfolder/image1.jpg': '', - '/photos/subfolder/image2.jpg': '', - '/image1.jpg': '', - }); - const options = new CrawlOptionsDto(); - options.pathsToCrawl = ['/photos/']; - options.recursive = true; - const paths: string[] = await crawlService.crawl(options); - expect(paths).toIncludeSameMembers([ - '/photos/image.jpg', - '/photos/subfolder/image1.jpg', - '/photos/subfolder/image2.jpg', - ]); - }); - - it('should filter file extensions', async () => { - mockfs({ - '/photos/image.jpg': '', - '/photos/image.txt': '', - '/photos/1': '', - }); - const options = new CrawlOptionsDto(); - options.pathsToCrawl = ['/photos/']; - const paths: string[] = await crawlService.crawl(options); - expect(paths).toIncludeSameMembers(['/photos/image.jpg']); - }); - - it('should include photo and video extensions', async () => { - mockfs({ - '/photos/image.jpg': '', - '/photos/image.jpeg': '', - '/photos/image.heic': '', - '/photos/image.heif': '', - '/photos/image.png': '', - '/photos/image.gif': '', - '/photos/image.tif': '', - '/photos/image.tiff': '', - '/photos/image.webp': '', - '/photos/image.dng': '', - '/photos/image.nef': '', - '/videos/video.mp4': '', - '/videos/video.mov': '', - '/videos/video.webm': '', - }); - - const options = new CrawlOptionsDto(); - options.pathsToCrawl = ['/photos/', '/videos/']; - const paths: string[] = await crawlService.crawl(options); - - expect(paths).toIncludeSameMembers([ - '/photos/image.jpg', - '/photos/image.jpeg', - '/photos/image.heic', - '/photos/image.heif', - '/photos/image.png', - '/photos/image.gif', - '/photos/image.tif', - '/photos/image.tiff', - '/photos/image.webp', - '/photos/image.dng', - '/photos/image.nef', - '/videos/video.mp4', - '/videos/video.mov', - '/videos/video.webm', - ]); - }); - - it('should check file extensions without case sensitivity', async () => { - mockfs({ - '/photos/image.jpg': '', - '/photos/image.Jpg': '', - '/photos/image.jpG': '', - '/photos/image.JPG': '', - '/photos/image.jpEg': '', - '/photos/image.TIFF': '', - '/photos/image.tif': '', - '/photos/image.dng': '', - '/photos/image.NEF': '', - }); - - const options = new CrawlOptionsDto(); - options.pathsToCrawl = ['/photos/']; - const paths: string[] = await crawlService.crawl(options); - expect(paths).toIncludeSameMembers([ - '/photos/image.jpg', - '/photos/image.Jpg', - '/photos/image.jpG', - '/photos/image.JPG', - '/photos/image.jpEg', - '/photos/image.TIFF', - '/photos/image.tif', - '/photos/image.dng', - '/photos/image.NEF', - ]); - }); +describe(CrawlService.name, () => { + const sut = new CrawlService( + ['.jpg', '.jpeg', '.png', '.heif', '.heic', '.tif', '.nef', '.webp', '.tiff', '.dng', '.gif'], + ['.mov', '.mp4', '.webm'], + ); afterEach(() => { mockfs.restore(); }); + + describe('crawl', () => { + for (const { test, options, files } of tests) { + it(test, async () => { + mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, '']))); + + const actual = await sut.crawl(options); + const expected = Object.entries(files) + .filter((entry) => entry[1]) + .map(([file]) => file); + + expect(actual.sort()).toEqual(expected.sort()); + }); + } + }); }); diff --git a/cli/src/services/crawl.service.ts b/cli/src/services/crawl.service.ts index bb90a8d0a..28d7fb912 100644 --- a/cli/src/services/crawl.service.ts +++ b/cli/src/services/crawl.service.ts @@ -1,47 +1,28 @@ import { CrawlOptionsDto } from 'src/cores/dto/crawl-options-dto'; -import { ACCEPTED_FILE_EXTENSIONS } from '../cores'; import { glob } from 'glob'; -import * as fs from 'fs'; export class CrawlService { - public async crawl(crawlOptions: CrawlOptionsDto): Promise { - const pathsToCrawl: string[] = crawlOptions.pathsToCrawl; + private readonly extensions!: string[]; - const directories: string[] = []; - const crawledFiles: string[] = []; + constructor(image: string[], video: string[]) { + this.extensions = image.concat(video).map((extension) => extension.replace('.', '')); + } - for await (const currentPath of pathsToCrawl) { - const stats = await fs.promises.stat(currentPath); - if (stats.isFile() || stats.isSymbolicLink()) { - crawledFiles.push(currentPath); - } else { - directories.push(currentPath); - } + crawl(crawlOptions: CrawlOptionsDto): Promise { + const { pathsToCrawl, exclusionPatterns, includeHidden } = crawlOptions; + if (!pathsToCrawl) { + return Promise.resolve([]); } - let searchPattern: string; - if (directories.length === 1) { - searchPattern = directories[0]; - } else if (directories.length === 0) { - return crawledFiles; - } else { - searchPattern = '{' + directories.join(',') + '}'; - } + const base = pathsToCrawl.length === 1 ? pathsToCrawl[0] : `{${pathsToCrawl.join(',')}}`; + const extensions = `*{${this.extensions}}`; - if (crawlOptions.recursive) { - searchPattern = searchPattern + '/**/'; - } - - searchPattern = `${searchPattern}/*.{${ACCEPTED_FILE_EXTENSIONS.join(',')}}`; - - const globbedFiles = await glob(searchPattern, { + return glob(`${base}/**/${extensions}`, { + absolute: true, nocase: true, nodir: true, - ignore: crawlOptions.excludePatterns, + dot: includeHidden, + ignore: exclusionPatterns, }); - - const returnedFiles = crawledFiles.concat(globbedFiles); - returnedFiles.sort(); - return returnedFiles; } } diff --git a/cli/src/services/index.ts b/cli/src/services/index.ts index 15aecfa27..9f0d5f958 100644 --- a/cli/src/services/index.ts +++ b/cli/src/services/index.ts @@ -1,2 +1 @@ -export * from './upload.service'; export * from './crawl.service'; diff --git a/cli/src/services/session.service.ts b/cli/src/services/session.service.ts index fb008b128..d1c9d789c 100644 --- a/cli/src/services/session.service.ts +++ b/cli/src/services/session.service.ts @@ -46,7 +46,7 @@ export class SessionService { // Check if server and api key are valid const { data: userInfo } = await this.api.userApi.getMyUserInfo().catch((error) => { - throw new LoginError(`Failed to connect to the server: ${error.message}`); + throw new LoginError(`Failed to connect to server ${instanceUrl}: ${error.message}`); }); console.log(`Logged in as ${userInfo.email}`); @@ -78,7 +78,7 @@ export class SessionService { private async ping(): Promise { const { data: pingResponse } = await this.api.serverInfoApi.pingServer().catch((error) => { - throw new Error(`Failed to connect to the server: ${error.message}`); + throw new Error(`Failed to connect to server ${this.api.apiConfiguration.instanceUrl}: ${error.message}`); }); if (pingResponse.res !== 'pong') { diff --git a/cli/src/services/upload.service.spec.ts b/cli/src/services/upload.service.spec.ts deleted file mode 100644 index d4a0e4d3a..000000000 --- a/cli/src/services/upload.service.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { UploadService } from './upload.service'; -import axios from 'axios'; -import FormData from 'form-data'; -import { ApiConfiguration } from '../cores/api-configuration'; - -jest.mock('axios', () => jest.fn()); - -describe('UploadService', () => { - let uploadService: UploadService; - - beforeEach(() => { - const apiConfiguration = new ApiConfiguration('https://example.com/api', 'key'); - - uploadService = new UploadService(apiConfiguration); - }); - - it('should call axios', async () => { - const data = new FormData(); - - await uploadService.upload(data); - - expect(axios).toHaveBeenCalled(); - }); -}); diff --git a/cli/src/services/upload.service.ts b/cli/src/services/upload.service.ts deleted file mode 100644 index c059b3417..000000000 --- a/cli/src/services/upload.service.ts +++ /dev/null @@ -1,65 +0,0 @@ -import axios, { AxiosRequestConfig } from 'axios'; -import FormData from 'form-data'; -import { ApiConfiguration } from '../cores/api-configuration'; - -export class UploadService { - private readonly uploadConfig: AxiosRequestConfig; - private readonly checkAssetExistenceConfig: AxiosRequestConfig; - private readonly importConfig: AxiosRequestConfig; - - constructor(apiConfiguration: ApiConfiguration) { - this.uploadConfig = { - method: 'post', - maxRedirects: 0, - url: `${apiConfiguration.instanceUrl}/asset/upload`, - headers: { - 'x-api-key': apiConfiguration.apiKey, - }, - maxContentLength: Number.POSITIVE_INFINITY, - maxBodyLength: Number.POSITIVE_INFINITY, - }; - - this.importConfig = { - method: 'post', - maxRedirects: 0, - url: `${apiConfiguration.instanceUrl}/asset/import`, - headers: { - 'x-api-key': apiConfiguration.apiKey, - 'Content-Type': 'application/json', - }, - maxContentLength: Number.POSITIVE_INFINITY, - maxBodyLength: Number.POSITIVE_INFINITY, - }; - - this.checkAssetExistenceConfig = { - method: 'post', - maxRedirects: 0, - url: `${apiConfiguration.instanceUrl}/asset/bulk-upload-check`, - headers: { - 'x-api-key': apiConfiguration.apiKey, - 'Content-Type': 'application/json', - }, - }; - } - - public checkIfAssetAlreadyExists(path: string, checksum: string) { - this.checkAssetExistenceConfig.data = JSON.stringify({ assets: [{ id: path, checksum: checksum }] }); - - // TODO: retry on 500 errors? - return axios(this.checkAssetExistenceConfig); - } - - public upload(data: FormData) { - this.uploadConfig.data = data; - - // TODO: retry on 500 errors? - return axios(this.uploadConfig); - } - - public import(data: any) { - this.importConfig.data = data; - - // TODO: retry on 500 errors? - return axios(this.importConfig); - } -} diff --git a/cli/tsconfig.json b/cli/tsconfig.json index 0ef386a56..b44be15d1 100644 --- a/cli/tsconfig.json +++ b/cli/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "module": "commonjs", + "module": "Node16", "strict": true, "declaration": true, "removeComments": true, @@ -8,7 +8,7 @@ "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, - "target": "es2017", + "target": "es2022", "moduleResolution": "node16", "sourceMap": true, "outDir": "./dist", diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 000000000..437aa274c --- /dev/null +++ b/docker/README.md @@ -0,0 +1,5 @@ +> [!CAUTION] +> Make sure to use the docker-compose.yml of the current release: +> https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml +> +> The compose file on main may not be compatible with the latest release. diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 2d44a1f07..247087aaf 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -1,30 +1,39 @@ +# See: +# - https://immich.app/docs/developer/setup +# - https://immich.app/docs/developer/troubleshooting + version: "3.8" +name: immich-dev + +x-server-build: &server-common + image: immich-server-dev:latest + build: + context: ../ + dockerfile: server/Dockerfile + target: dev + volumes: + - ../server:/usr/src/app + - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload + - /usr/src/app/node_modules + - /etc/localtime:/etc/localtime:ro + env_file: + - .env + environment: + - NODE_ENV=development + ulimits: + nofile: + soft: 1048576 + hard: 1048576 + services: immich-server: container_name: immich_server - image: immich-server-dev:latest - build: - context: ../server - dockerfile: Dockerfile - target: builder command: npm run start:debug immich - volumes: - - ../server:/usr/src/app - - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - - /usr/src/app/node_modules - - /etc/localtime:/etc/localtime:ro + <<: *server-common ports: - 3001:3001 - 9230:9230 - env_file: - - .env - environment: - - NODE_ENV=development - ulimits: - nofile: - soft: 1048576 - hard: 1048576 depends_on: - redis - database @@ -32,30 +41,13 @@ services: immich-microservices: container_name: immich_microservices - image: immich-microservices:latest + command: npm run start:debug microservices + <<: *server-common # extends: # file: hwaccel.yml # service: hwaccel - build: - context: ../server - dockerfile: Dockerfile - target: builder - command: npm run start:debug microservices - volumes: - - ../server:/usr/src/app - - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload - - /usr/src/app/node_modules - - /etc/localtime:/etc/localtime:ro - env_file: - - .env ports: - 9231:9230 - environment: - - NODE_ENV=development - ulimits: - nofile: - soft: 1048576 - hard: 1048576 depends_on: - database - immich-server @@ -67,16 +59,11 @@ services: build: context: ../web dockerfile: Dockerfile - target: dev - command: npm run dev --host + command: "node ./node_modules/.bin/vite dev --host 0.0.0.0 --port 3000" env_file: - .env - environment: - # Rename these values for svelte public interface - - PUBLIC_IMMICH_SERVER_URL=${IMMICH_SERVER_URL} - - PUBLIC_IMMICH_API_URL_EXTERNAL=${IMMICH_API_URL_EXTERNAL} ports: - - 3000:3000 + - 2283:3000 - 24678:24678 volumes: - ../web:/usr/src/app @@ -121,11 +108,11 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3 + image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46 database: container_name: immich_postgres - image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441 + image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07 env_file: - .env environment: @@ -137,22 +124,5 @@ services: ports: - 5432:5432 - immich-proxy: - container_name: immich_proxy - image: immich-proxy-dev:latest - environment: - # Make sure these values get passed through from the env file - - IMMICH_SERVER_URL - - IMMICH_WEB_URL - build: - context: ../nginx - dockerfile: Dockerfile - ports: - - 2283:8080 - depends_on: - - immich-server - - immich-web - restart: unless-stopped - volumes: model-cache: diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 030429985..93bfc5f64 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -1,23 +1,44 @@ version: "3.8" +name: immich-prod + +x-server-build: &server-common + image: immich-server:latest + build: + context: ../ + dockerfile: server/Dockerfile + volumes: + - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload + - /etc/localtime:/etc/localtime:ro + env_file: + - .env + restart: always + services: immich-server: container_name: immich_server - image: immich-server:latest - build: - context: ../server - dockerfile: Dockerfile - command: ["./start-server.sh"] - volumes: - - ${UPLOAD_LOCATION}:/usr/src/app/upload - - /etc/localtime:/etc/localtime:ro - env_file: - - .env + command: [ "./start-server.sh" ] + <<: *server-common + ports: + - 2283:3001 depends_on: - redis - database - typesense + immich-microservices: + container_name: immich_microservices + command: [ "./start-microservices.sh" ] + <<: *server-common + # extends: + # file: hwaccel.yml + # service: hwaccel + depends_on: + - redis + - database + - typesense + - immich-server + immich-machine-learning: container_name: immich_machine_learning image: immich-machine-learning:latest @@ -25,45 +46,11 @@ services: context: ../machine-learning dockerfile: Dockerfile volumes: - - ${UPLOAD_LOCATION}:/usr/src/app/upload - model-cache:/cache env_file: - .env restart: always - immich-microservices: - container_name: immich_microservices - image: immich-microservices:latest - # extends: - # file: hwaccel.yml - # service: hwaccel - build: - context: ../server - dockerfile: Dockerfile - command: ["./start-microservices.sh"] - volumes: - - ${UPLOAD_LOCATION}:/usr/src/app/upload - - /etc/localtime:/etc/localtime:ro - env_file: - - .env - depends_on: - - database - - immich-server - - typesense - restart: always - - immich-web: - container_name: immich_web - image: immich-web:latest - build: - context: ../web - dockerfile: Dockerfile - env_file: - - .env - restart: always - depends_on: - - immich-server - typesense: container_name: immich_typesense image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd @@ -73,17 +60,17 @@ services: # remove this to get debug messages - GLOG_minloglevel=1 volumes: - - tsdata:/data + - ${UPLOAD_LOCATION}/typesense:/data restart: always redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3 + image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46 restart: always database: container_name: immich_postgres - image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441 + image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07 env_file: - .env environment: @@ -91,28 +78,8 @@ services: POSTGRES_USER: ${DB_USERNAME} POSTGRES_DB: ${DB_DATABASE_NAME} volumes: - - pgdata:/var/lib/postgresql/data - restart: always - - immich-proxy: - container_name: immich_proxy - image: immich-proxy:latest - environment: - # Make sure these values get passed through from the env file - - IMMICH_SERVER_URL - - IMMICH_WEB_URL - build: - context: ../nginx - dockerfile: Dockerfile - ports: - - 2283:8080 - logging: - driver: none - depends_on: - - immich-server + - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data restart: always volumes: - pgdata: model-cache: - tsdata: diff --git a/docker/docker-compose.test.yml b/docker/docker-compose.test.yml index df965aa1f..daa6524f8 100644 --- a/docker/docker-compose.test.yml +++ b/docker/docker-compose.test.yml @@ -6,9 +6,9 @@ services: immich-server: image: immich-server-dev:latest build: - context: ../server - dockerfile: Dockerfile - target: builder + context: ../ + dockerfile: server/Dockerfile + target: dev command: npm run test:e2e volumes: - ../server:/usr/src/app @@ -23,7 +23,7 @@ services: - database database: - image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441 + image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07 command: -c fsync=off environment: POSTGRES_PASSWORD: postgres diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index f020f8e34..9452975fb 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,15 +1,27 @@ version: "3.8" +# +# WARNING: Make sure to use the docker-compose.yml of the current release: +# +# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml +# +# The compose file on main may not be compatible with the latest release. +# + +name: immich + services: immich-server: container_name: immich_server image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} - command: ["start.sh", "immich"] + command: [ "start.sh", "immich" ] volumes: - ${UPLOAD_LOCATION}:/usr/src/app/upload - /etc/localtime:/etc/localtime:ro env_file: - .env + ports: + - 2283:3001 depends_on: - redis - database @@ -22,7 +34,7 @@ services: # extends: # file: hwaccel.yml # service: hwaccel - command: ["start.sh", "microservices"] + command: [ "start.sh", "microservices" ] volumes: - ${UPLOAD_LOCATION}:/usr/src/app/upload - /etc/localtime:/etc/localtime:ro @@ -43,13 +55,6 @@ services: - .env restart: always - immich-web: - container_name: immich_web - image: ghcr.io/immich-app/immich-web:${IMMICH_VERSION:-release} - env_file: - - .env - restart: always - typesense: container_name: immich_typesense image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd @@ -64,12 +69,12 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3 + image: redis:6.2-alpine@sha256:60e49e22fa5706cd8df7d5e0bc50ee9bab7c608039fa653c4d961014237cca46 restart: always database: container_name: immich_postgres - image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441 + image: postgres:14-alpine@sha256:6a0e35296341e676fe6bd8d236c72afffe2dfe3d7eb9c2405c0f3fc04500cd07 env_file: - .env environment: @@ -80,16 +85,6 @@ services: - pgdata:/var/lib/postgresql/data restart: always - immich-proxy: - container_name: immich_proxy - image: ghcr.io/immich-app/immich-proxy:${IMMICH_VERSION:-release} - ports: - - 2283:8080 - depends_on: - - immich-server - - immich-web - restart: always - volumes: pgdata: model-cache: diff --git a/docs/docs/FAQ.md b/docs/docs/FAQ.md index 07a109d46..50ee2c112 100644 --- a/docs/docs/FAQ.md +++ b/docs/docs/FAQ.md @@ -12,9 +12,9 @@ sidebar_position: 7 | ![cloud-cross](/img/cloud-off.svg) | Asset is only available locally and has not yet been backed up | | ![cloud-done](/img/cloud-done.svg) | Asset was uploaded from this device and is now backed up in the cloud/server and still available in original on the device | -### How can I sync an existing directory with Immich's server? +### Can I add my existing photo library? -Immich doesn't have two-way synchronization ([yet](https://github.com/immich-app/immich/discussions/1006)), but the [command line tool](/docs/features/bulk-upload.md) can bulk upload items from a directory to Immich. +Yes, with an [external library](/docs/features/libraries.md). ### Why are only photos and not videos being uploaded to Immich? diff --git a/docs/docs/administration/reverse-proxy.md b/docs/docs/administration/reverse-proxy.md index 2debb44a1..53e1e0c61 100644 --- a/docs/docs/administration/reverse-proxy.md +++ b/docs/docs/administration/reverse-proxy.md @@ -1,21 +1,6 @@ # Reverse Proxy -When deploying Immich it is important to understand that a reverse proxy is required in front of the server and web container. The reverse proxy acts as an intermediary between the user and container, forwarding requests to the correct container based on the URL path. - -## Default Reverse Proxy - -Immich provides a default nginx reverse proxy preconfigured to perform the correct routing and set the necessary headers for the server and web container to use. These headers are crucial to redirect to the correct URL and determine the client's IP address. - -## Using a Different Reverse Proxy - -While the reverse proxy provided by Immich works well for basic deployments, some users may want to use a different reverse proxy. Fortunately, Immich is flexible enough to accommodate different reverse proxies. Users can either: - -1. Add another reverse proxy on top of Immich's reverse proxy -2. Completely replace the default reverse proxy - -## Adding a Custom Reverse Proxy - -Users can deploy a custom reverse proxy that forwards requests to Immich's reverse proxy. This way, the new reverse proxy can handle TLS termination, load balancing, or other advanced features, while still delegating routing decisions to Immich's reverse proxy. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich. +Users can deploy a custom reverse proxy that forwards requests to Immich. This way, the reverse proxy can handle TLS termination, load balancing, or other advanced features. All reverse proxies between Immich and the user must forward all headers and set the `Host`, `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-For` headers to their appropriate values. Additionally, your reverse proxy should allow for big enough uploads. By following these practices, you ensure that all custom reverse proxies are fully compatible with Immich. ### Nginx example config @@ -43,7 +28,3 @@ server { } } ``` - -## Replacing the Default Reverse Proxy - -Replacing Immich's default reverse proxy is an advanced deployment and support may be limited. When replacing Immich's default proxy it is important to ensure that requests to `/api/*` are routed to the server container and all other requests to the web container. Additionally, the previously mentioned headers should be configured accordingly. You may find our [nginx configuration file](https://github.com/immich-app/immich/blob/main/nginx/templates/default.conf.template) a helpful reference. diff --git a/docs/docs/administration/server-commands.md b/docs/docs/administration/server-commands.md index d8cd2119d..fc235f5a5 100644 --- a/docs/docs/administration/server-commands.md +++ b/docs/docs/administration/server-commands.md @@ -51,8 +51,7 @@ immich-admin list-users { id: 'e65e6f88-2a30-4dbe-8dd9-1885f4889b53', email: 'immich@example.com.com', - firstName: 'Immich', - lastName: 'Admin', + name: 'Immich Admin', storageLabel: 'admin', externalPath: null, profileImagePath: 'upload/profile/e65e6f88-2a30-4dbe-8dd9-1885f4889b53/e65e6f88-2a30-4dbe-8dd9-1885f4889b53.jpg', diff --git a/docs/docs/developer/architecture.md b/docs/docs/developer/architecture.md index 36e6ea939..0ade08227 100644 --- a/docs/docs/developer/architecture.md +++ b/docs/docs/developer/architecture.md @@ -34,7 +34,7 @@ The web app is a [TypeScript](https://www.typescriptlang.org/) project that uses ### CLI -The CLI is a [TypeScript](https://www.typescriptlang.org/) project that parses command line arguments to programmatically upload/import assets to an Immich server. See [Bulk Upload](/docs/features/bulk-upload.md) for more information about its usage. +The Immich CLI is an [npm](https://www.npmjs.com/) package that lets users control their Immich instance from the command line. It uses the API to perform various tasks, especially uploading assets. See the [CLI documentation](/docs/features/command-line-interface.md) for more information. ## Server diff --git a/docs/docs/developer/database-migrations.md b/docs/docs/developer/database-migrations.md index f94e2ca4f..6753e864c 100644 --- a/docs/docs/developer/database-migrations.md +++ b/docs/docs/developer/database-migrations.md @@ -9,6 +9,6 @@ npm run typeorm:migrations:generate ./src/infra/ ``` 2. Check if the migration file makes sense. -3. Move the migration file to folder `./src/infra/database/migrations` in your code editor. +3. Move the migration file to folder `./server/src/infra/migrations` in your code editor. The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately. diff --git a/docs/docs/developer/directories.md b/docs/docs/developer/directories.md index 77f8e7960..3ec483294 100644 --- a/docs/docs/developer/directories.md +++ b/docs/docs/developer/directories.md @@ -17,6 +17,5 @@ Our [GitHub Repository](https://github.com/immich-app/immich) is a [monorepo](ht | `machine-learning/` | Source code for the `immich-machine-learning` docker image | | `misc/release/` | Scripts for version pumps and draft releases | | `mobile/` | Source code for the mobile app, both Android and iOS | -| `nginx/` | Source code for the `immich-proxy` docker image | | `server/` | Source code for the `immich-server` docker image | -| `web/` | Source code for the `immich-web` docker image | +| `web/` | Source code for the `web` | diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index e712426ff..53e6cdd27 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -52,7 +52,7 @@ If you only want to do web development connected to an existing, remote backend, 3. Start the web development server ``` -PUBLIC_IMMICH_SERVER_URL=https://demo.immich.app/api npm run dev +IMMICH_SERVER_URL=https://demo.immich.app/api npm run dev ``` ## IDE setup @@ -61,9 +61,15 @@ PUBLIC_IMMICH_SERVER_URL=https://demo.immich.app/api npm run dev Setting these in the IDE give a better developer experience, auto-formatting code on save, and providing instant feedback on lint issues. +### Dart Code Metris + +The mobile app uses DCM (Dart Code Metrics) for linting and metrics calculation. Please refer to the [Getting Started](https://dcm.dev/docs/getting-started/#installation) page for more information on setting up DCM + +Note: Activating the license is not required. + ### VSCode -Install `Flutter`, `Prettier`, `ESLint` and `Svelte` extensions. +Install `Flutter`, `DCM`, `Prettier`, `ESLint` and `Svelte` extensions. in User `settings.json` (`cmd + shift + p` and search for `Open User Settings JSON`) add the following: diff --git a/docs/docs/developer/troubleshooting.md b/docs/docs/developer/troubleshooting.md new file mode 100644 index 000000000..8663f5356 --- /dev/null +++ b/docs/docs/developer/troubleshooting.md @@ -0,0 +1,15 @@ +# Troubleshooting + +:::tip +A great option to get assistance with troubleshooting is to join our [Discord](https://discord.gg/D8JsnBEuKb) server, where we have a dedicated channel for `#contributing`. +::: + +## Known Issues + +### Running on Windows + +Running Immich on Windows can be frustrating and there are lots of ways it can go wrong. Where possible we recommend using Docker on Linux. However, several people have had success running Immich on Windows using Docker via WSL2. + +### NTFS Mounted Volumes + +The docker-compose.dev.yml and docker-compose.prod.yml use volume mounts for the postgres database. On start-up, postgres will try to `chown` the data directory, but fail. See [this post](https://forums.docker.com/t/data-directory-var-lib-postgresql-data-pgdata-has-wrong-ownership/17963/24) for more information about this issue and possible solutions. diff --git a/docs/docs/features/bulk-upload.md b/docs/docs/features/bulk-upload.md deleted file mode 100644 index 5923e5c16..000000000 --- a/docs/docs/features/bulk-upload.md +++ /dev/null @@ -1,109 +0,0 @@ -# Bulk Upload (Using the CLI) - -You can use the CLI to upload an existing gallery to the Immich server - -[Immich CLI Repository](https://github.com/immich-app/CLI) - -## Requirements - -- Node.js 16 or above -- Npm - -## Installation - -```bash -npm i -g immich -``` - -Pre-installed on the `immich-server` container and can be easily accessed through - -``` -immich -``` - -### Options - -| Parameter | Description | -| ---------------- | ------------------------------------------------------------------- | -| --yes / -y | Assume yes on all interactive prompts | -| --recursive / -r | Include subfolders | -| --delete / -da | Delete local assets after upload | -| --key / -k | User's API key | -| --server / -s | Immich's server address | -| --threads / -t | Number of threads to use (Default 5) | -| --album/ -al | Create albums for assets based on the parent folder or a given name | - -## Quick Start - -Specify user's credential, Immich's server address and port and the directory you would like to upload videos/photos from. - -``` -immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api file1.jpg file2.jpg -``` - -By default, subfolders are not included. To upload a directory including subfolder, use the --recursive option: - -``` -immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive directory/ -``` - -### Obtain the API Key - -The API key can be obtained in the user setting panel on the web interface. - -![Obtain Api Key](./img/obtain-api-key.png) - ---- - -### Run via Docker - -You can run the CLI inside of a docker container to avoid needing to install anything. - -:::caution Running inside Docker -Be aware that as this runs inside a container, you need to mount the folder from which you want to import into the container. -::: - -```bash title="Upload current directory" -cd /DIRECTORY/WITH/IMAGES -docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api -``` - -```bash title="Upload target directory" -docker run -it --rm -v "/DIRECTORY/WITH/IMAGES:/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api -``` - -```bash title="Create an alias" -alias immich='docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest' -immich upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api -``` - -:::tip Internal networking -If you are running the CLI container on the same machine as your Immich server, you may not be able to reach the external address. In that case, try the following steps: - -1. Find the internal Docker network used by Immich via `docker network ls`. -2. Adapt the above command to pass the `--network ` argument to `docker run`, substituting `` with the result from step 1. -3. Use `--server http://immich-server:3001` for the upload command instead of the external address. - -```bash title="Upload to internal address" -docker run --network immich_default -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://immich-server:3001 -``` - -::: - -### Run from source - -```bash title="Clone Repository" -git clone https://github.com/immich-app/CLI -``` - -```bash title="Install dependencies" -npm install -``` - -```bash title="Build the project" -npm run build -``` - -```bash title="Run the command" -node bin/index.js upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive your/asset/directory -``` diff --git a/docs/docs/features/command-line-interface.md b/docs/docs/features/command-line-interface.md new file mode 100644 index 000000000..4d4a0e16e --- /dev/null +++ b/docs/docs/features/command-line-interface.md @@ -0,0 +1,139 @@ +# The Immich CLI + +Immich has a CLI that allows you to perform certain actions from the command line. This CLI replaces the [legacy CLI](https://github.com/immich-app/CLI) that was previously available. The CLI is hosted in the [cli folder of the the main Immich github repository](https://github.com/immich-app/immich/tree/main/cli). + +## Features + +- Upload photos and videos to Immich +- Check server version + +More features are planned for the future. + +:::tip Google Photos Takeout +If you are looking to import your Google Photos takeout, we recommed this community maintained tool [immich-go](https://github.com/simulot/immich-go) +::: + +## Requirements + +- Node.js 20.0 or above +- Npm + +## Installation + +```bash +npm i -g @immich/cli +``` + +NOTE: if you previously installed the legacy CLI, you will need to uninstall it first: + +```bash +npm uninstall -g immich +``` + +## Usage + +``` +immich +``` + +``` +Usage: immich [options] [command] + +Immich command line interface + +Options: + -h, --help display help for command + +Commands: + upload [options] [paths...] Upload assets + server-info Display server information + login-key [instanceUrl] [apiKey] Login using an API key + logout Remove stored credentials + help [command] display help for command +``` + +## Commands + +The upload command supports the following options: + +``` +Usage: immich upload [options] [paths...] + +Upload assets + +Arguments: + paths One or more paths to assets to be uploaded + +Options: + -r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE) + -i, --ignore [paths...] Paths to ignore (env: IMMICH_IGNORE_PATHS) + -h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH) + -a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM) + -n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN) + --delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS) + --help display help for command +``` + +Note that the above options can read from environment variables as well. + +## Quick Start + +You begin by authenticating to your Immich server. + +```bash +immich login-key [instanceUrl] [apiKey] +``` + +For instance, + +```bash +immich login-key http://192.168.1.216:2283/api HFEJ38DNSDUEG +``` + +This will store your credentials in a file in your home directory. Please keep the file secure, either by performing the logout command after you are done, or deleting it manually. + +Once you are authenticated, you can upload assets to your Immich server. + +```bash +immich upload file1.jpg file2.jpg +``` + +By default, subfolders are not included. To upload a directory including subfolder, use the --recursive option: + +```bash +immich upload --recursive directory/ +``` + +If you are unsure what will happen, you can use the `--dry-run` option to see what would happen without actually performing any actions. + +```bash +immich upload --dry-run --recursive directory/ +``` + +By default, the upload command will hash the files before uploading them. This is to avoid uploading the same file multiple times. If you are sure that the files are unique, you can skip this step by passing the `--skip-hash` option. Note that Immich always performs its own deduplication through hashing, so this is merely a performance consideration. If you have good bandwidth it might be faster to skip hashing. + +```bash +immich upload --skip-hash --recursive directory/ +``` + +You can automatically create albums based on the folder name by passing the `--album` option. This will automatically create albums for each uploaded asset based on the name of the folder they are in. + +```bash +immich upload --album --recursive directory/ +``` + +It is possible to skip assets matching a glob pattern by passing the `--ignore` option. See [the library documentation](docs/features/libraries.md) on how to use glob patterns. You can add several exclusion patterns if needed. + +```bash +immich upload --ignore **/Raw/** --recursive directory/ +``` + +```bash +immich upload --ignore **/Raw/** **/*.tif --recursive directory/ +``` + +### Obtain the API Key + +The API key can be obtained in the user setting panel on the web interface. + +![Obtain Api Key](./img/obtain-api-key.png) diff --git a/docs/docs/features/facial-recognition.md b/docs/docs/features/facial-recognition.md index b9efba428..2095df587 100644 --- a/docs/docs/features/facial-recognition.md +++ b/docs/docs/features/facial-recognition.md @@ -1,5 +1,7 @@ # Facial Recognition +## Overview + Immich recognizes faces in your photos and videos and groups them together. You can then assign names to the faces and search for them. The list of people is shown in the Explore page. @@ -13,3 +15,16 @@ Upon clicking on a person, a list of assets that contain their face will be show The asset detail view will also show the faces that are recognized in the asset. + +## Actions + +Additional actions you can do with a detected person are: + +- Change the feature face photo of the person +- Set date of birth +- Merge two or more detected faces into one person +- Hide face + +It can be found from the app bar when you access the detial view of a person + + diff --git a/docs/docs/features/img/facial-recognition-4.png b/docs/docs/features/img/facial-recognition-4.png new file mode 100644 index 000000000..718ee6d45 Binary files /dev/null and b/docs/docs/features/img/facial-recognition-4.png differ diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index f518a33a5..1437fb863 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -75,7 +75,7 @@ Some basic examples: - `*.tif` will exclude all files with the extension `.tif` - `hidden.jpg` will exclude all files named `hidden.jpg` - `**/Raw/**` will exclude all files in any directory named `Raw` -- `*.(tif,jpg)` will exclude all files with the extension `.tif` or `.jpg` +- `*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg` ### Nightly job diff --git a/docs/docs/features/mobile-app.mdx b/docs/docs/features/mobile-app.mdx index 9a79ccf17..d858fc18a 100644 --- a/docs/docs/features/mobile-app.mdx +++ b/docs/docs/features/mobile-app.mdx @@ -1,13 +1,9 @@ import MobileAppDownload from '../partials/_mobile-app-download.md'; import MobileAppLogin from '../partials/_mobile-app-login.md'; -import MobileAppBackup from '../partials/_mobile-app-login.md'; +import MobileAppBackup from '../partials/_mobile-app-backup.md'; # Mobile App -:::tip -To upload from other devices, try using the [Bulk Upload CLI](/docs/features/bulk-upload.md). -::: - ## Download diff --git a/docs/docs/guides/docker-help.md b/docs/docs/guides/docker-help.md index 7ea0f6d67..37fdf88cb 100644 --- a/docs/docs/guides/docker-help.md +++ b/docs/docs/guides/docker-help.md @@ -14,8 +14,6 @@ docker exec -it # attach to a container with a c docker exec -it immich_server sh docker exec -it immich_microservices sh docker exec -it immich_machine_learning sh -docker exec -it immich_web sh -docker exec -it immich_proxy sh ``` ## Logs @@ -26,8 +24,6 @@ docker logs # see the logs for a specific container (by id docker logs immich_server docker logs immich_microservices docker logs immich_machine_learning -docker logs immich_web -docker logs immich_proxy ``` :::tip Follow a log diff --git a/docs/docs/guides/machine-learning.md b/docs/docs/guides/machine-learning.md index f16c23a1e..587c080ab 100644 --- a/docs/docs/guides/machine-learning.md +++ b/docs/docs/guides/machine-learning.md @@ -2,7 +2,7 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.md#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine-learning container on a more powerful system (e.g. your laptop or desktop computer): -- Set `IMMICH_MACHINE_LEARNING_URL` to point to the designated ML system, e.g. `http://workstation:3003`. +- Set the URL in Machine Learning Settings on the Admin Settings page to point to the designated ML system, e.g. `http://workstation:3003`. - Copy the following `docker-compose.yml` to your ML system. - Start the container by running `docker-compose up -d` or `docker compose up -d` (depending on your Docker version). diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md new file mode 100644 index 000000000..b72bf5a03 --- /dev/null +++ b/docs/docs/guides/remote-access.md @@ -0,0 +1,58 @@ +# Remote Access + +This page gives a few pointers on how to access your Immich instance from outside your LAN. + +:::danger +Never forward port 2283 directly to the internet without additional configuration. This will expose the web interface via http to the internet, making you succeptible to [man in the middle](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) attacks. +::: + +## Option 1: VPN to home network + +You may use a VPN service to open an encrypted connection to your Immich instance. OpenVPN and Wireguard are two popular VPN solutions. Here is a guide on setting up VPN access to your server - [Pihole documentation](https://docs.pi-hole.net/guides/vpn/wireguard/overview/) + +### Pros: + +- Simple to set up and very secure. +- Single point of potential failure, i.e., the VPN software itself. Even if there is a zero-day vulnerability on Immich, you will not be at risk. +- Both Wireguard and OpenVPN are independently security-audited, so the risk of serious zero-day exploits are minimal. + +### Cons: + +- If you don't have a static IP address, you would need to set up a [Dynamic DNS](https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/). [DuckDNS](https://www.duckdns.org/) is a free DDNS provider. +- VPN software needs to be installed and active on both server-side and client-side. +- Requires you to open a port on your router to your server. + +## Option 2: Tailscale + +If you are unable to open a port on your router for Wireguard or OpenVPN to your server, [Tailscale](https://tailscale.com/) is a good option. Tailscale mediates a peer-to-peer wireguard tunnel between your server and remote device, even if one or both of them are behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation). + +### Pros + +- Minimal configuration needed on server and client sides. +- You are protected against zero-day vulnerabilities on Immich. + +### Cons + +- The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022. +- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) that permits up to 3 users and up to 100 devices. +- Tailscale needs to be installed and running on both server-side and client-side. + +## Option 3: Reverse Proxy + +A reverse proxy is a service that sits between web servers and clients. A reverse proxy can either be hosted on the server itself or remotely. Clients can connect to the reverse proxy via https, and the proxy relays data to Immich. This setup makes most sense if you have your own domain and want to access your Immich instance just like any other website, from outside your LAN. You can also use a DDNS provider like DuckDNS or no-ip if you don't have a domain. This configuration allows the Immich Android and iphone apps to connect to your server without a VPN or tailscale app on the client side. + +If you're hosting your own reverse proxy, [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) is a great option. An example configuration for Nginx is provided [here](https://immich.app/docs/administration/reverse-proxy). + +You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accesible, [Let's Encrypt](https://letsencrypt.org/) can provide a free certificate for your domain and is the recommended option. Alternatively, a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) allows you to encrypt your connection to Immich, but it raises a security warning on the client's browser. + +A remote reverse proxy like [Cloudflare](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/) increases security by hiding the server IP address, which makes targeted attacks like [DDoS](https://www.cloudflare.com/learning/ddos/what-is-a-ddos-attack/) harder. + +### Pros + +- No additional software needs to be installed client-side +- If you only need access to the web interface remotely, it is possible to set up access controls that shield you from zero-day vulnerabilities on Immich. [Cloudflare Access](https://www.cloudflare.com/zero-trust/products/access/) has a generous free tier. + +### Cons + +- Complex configuration +- Depending on your configuration, both the Immich web interface and API may be exposed to the internet. Immich is under very active developement and the existence of severe security vulnerabilities cannot be ruled out. diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index 195151a54..def99529c 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -17,6 +17,12 @@ The default configuration looks like this: "targetAudioCodec": "aac", "targetResolution": "720", "maxBitrate": "0", + "bframes": -1, + "refs": 0, + "gopSize": 0, + "npl": 0, + "temporalAQ": false, + "cqMode": "auto", "twoPass": false, "transcode": "required", "tonemap": "hable", @@ -44,9 +50,15 @@ The default configuration looks like this: "sidecar": { "concurrency": 5 }, + "library": { + "concurrency": 5 + }, "storageTemplateMigration": { "concurrency": 5 }, + "migration": { + "concurrency": 5 + }, "thumbnailGeneration": { "concurrency": 5 }, @@ -55,16 +67,16 @@ The default configuration looks like this: } }, "machineLearning": { - "classification": { - "minScore": 0.7, - "enabled": true, - "modelName": "microsoft/resnet-50" - }, "enabled": true, "url": "http://immich-machine-learning:3003", + "classification": { + "enabled": true, + "modelName": "microsoft/resnet-50", + "minScore": 0.9 + }, "clip": { "enabled": true, - "modelName": "ViT-B-32::openai" + "modelName": "ViT-B-32__openai" }, "facialRecognition": { "enabled": true, @@ -74,6 +86,14 @@ The default configuration looks like this: "minFaces": 1 } }, + "map": { + "enabled": true, + "tileUrl": "https://tile.openstreetmap.org/{z}/{x}/{y}.png" + }, + "reverseGeocoding": { + "enabled": true, + "citiesFileOverride": "cities500" + }, "oauth": { "enabled": false, "issuerUrl": "", @@ -96,8 +116,27 @@ The default configuration looks like this: "thumbnail": { "webpSize": 250, "jpegSize": 1440, - "quality": 90, + "quality": 80, "colorspace": "p3" + }, + "newVersionCheck": { + "enabled": true + }, + "trash": { + "enabled": true, + "days": 30 + }, + "theme": { + "customCss": "" + }, + "library": { + "scan": { + "enabled": true, + "cronExpression": "0 0 * * *" + } + }, + "stylesheets": { + "css": "" } } ``` diff --git a/docs/docs/install/docker-compose.md b/docs/docs/install/docker-compose.md index d09ba531f..58e13a95a 100644 --- a/docs/docs/install/docker-compose.md +++ b/docs/docs/install/docker-compose.md @@ -122,28 +122,6 @@ TYPESENSE_API_KEY=some-random-text PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server" -#################################################################################### -# Alternative Service Addresses - Optional -# -# This is an advanced feature for users who may be running their immich services on different hosts. -# It will not change which address or port that services bind to within their containers, but it will change where other services look for their peers. -# Note: immich-microservices is bound to 3002, but no references are made -#################################################################################### - -IMMICH_WEB_URL=http://immich-web:3000 -IMMICH_SERVER_URL=http://immich-server:3001 - -#################################################################################### -# Alternative API's External Address - Optional -# -# This is an advanced feature used to control the public server endpoint returned to clients during Well-known discovery. -# You should only use this if you want mobile apps to access the immich API over a custom URL. Do not include trailing slash. -# NOTE: At this time, the web app will not be affected by this setting and will continue to use the relative path: /api -# Examples: http://localhost:3001, http://immich-api.example.com, etc -#################################################################################### - -#IMMICH_API_URL_EXTERNAL=http://localhost:3001 - ################################################################################### # Immich Version - Optional # @@ -166,7 +144,7 @@ IMMICH_SERVER_URL=http://immich-server:3001 From the directory you created in Step 1, (which should now contain your customized `docker-compose.yml` and `.env` files) run `docker-compose up -d`. ```bash title="Start the containers using docker compose command" -docker-compose up -d # or `docker compose up -d` based on your docker-compose version +docker compose up -d ``` :::tip @@ -184,7 +162,7 @@ If `IMMICH_VERSION` is set, it will need to be updated to the latest or desired When a new version of Immich is [released](https://github.com/immich-app/immich/releases), the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file: ```bash title="Upgrade Immich" -docker-compose pull && docker-compose up -d # Or `docker compose up -d` +docker compose pull && docker compose up -d ``` :::caution Automatic Updates diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 403ff2c36..025568a67 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -63,21 +63,6 @@ These environment variables are used by the `docker-compose.yml` file and do **N | `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning | | `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning | -## URLs - -| Variable | Description | Default | Services | -| :------------------------- | :---------------------- | :-------------------------: | :--------- | -| `IMMICH_WEB_URL` | Immich Web URL | `http://immich-web:3000` | proxy | -| `IMMICH_SERVER_URL` | Immich Server URL | `http://immich-server:3001` | web, proxy | -| `PUBLIC_IMMICH_SERVER_URL` | Public Immich URL | `http://immich-server:3001` | web | -| `IMMICH_API_URL_EXTERNAL` | Immich API URL External | `/api` | web | - -:::info - -The above paths are modifying the internal paths of the containers. - -::: - ## Database | Variable | Description | Default | Services | @@ -188,19 +173,18 @@ Typesense URL example JSON before encoding: | Variable | Description | Default | Services | | :----------------------------------------------- | :---------------------------------------------------------------- | :-----------------: | :--------------- | -| `MACHINE_LEARNING_MODEL_TTL`\*1 | Inactivity time (s) before a model is unloaded (disabled if <= 0) | `0` | machine learning | +| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if <= 0) | `300` | machine learning | +| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if <= 0) | `10` | machine learning | | `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning | -| `MACHINE_LEARNING_REQUEST_THREADS`\*2 | Thread count of the request thread pool (disabled if <= 0) | number of CPU cores | machine learning | +| `MACHINE_LEARNING_REQUEST_THREADS`\*1 | Thread count of the request thread pool (disabled if <= 0) | number of CPU cores | machine learning | | `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning | | `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning | -| `MACHINE_LEARNING_WORKERS`\*3 | Number of worker processes to spawn | `1` | machine learning | +| `MACHINE_LEARNING_WORKERS`\*2 | Number of worker processes to spawn | `1` | machine learning | | `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` | machine learning | -\*1: This is an experimental feature. It may result in increased memory use over time when loading models repeatedly. +\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones. -\*2: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones. - -\*3: Since each process duplicates models in memory, changing this is not recommended unless you have abundant memory to go around. +\*2: Since each process duplicates models in memory, changing this is not recommended unless you have abundant memory to go around. :::info diff --git a/docs/docs/install/unraid.md b/docs/docs/install/unraid.md index 8b2638e39..cba15a12e 100644 --- a/docs/docs/install/unraid.md +++ b/docs/docs/install/unraid.md @@ -98,12 +98,12 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich" > Note: This can take several minutes depending on your Internet speed and Unraid hardware -9. Once on the Docker page you will see several Immich containers, one of them will be labelled `immich_proxy` and will have a port mapping. Visit the `IP:PORT` displayed in your web browser and you should see the Immich admin setup page. +9. Once on the Docker page you will see several Immich containers, one of them will be labelled `immich_web` and will have a port mapping. Visit the `IP:PORT` displayed in your web browser and you should see the Immich admin setup page.
@@ -112,12 +112,12 @@ alt="Go to Docker Tab and visit the address listed next to immich-proxy" Go to Docker Tab and visit the address listed next to immich-proxy Go to Docker Tab and visit the address listed next to immich-proxy
diff --git a/docs/docs/overview/support-the-project.md b/docs/docs/overview/support-the-project.md index 8819cdafd..7bd473eb1 100644 --- a/docs/docs/overview/support-the-project.md +++ b/docs/docs/overview/support-the-project.md @@ -12,8 +12,8 @@ If you feel like this is the right cause and the app is something you see yourse ## Donation -- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502) -- One-time donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) +- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/immich-app) +- One-time donation via [GitHub Sponsors](https://github.com/sponsors/immich-app?frequency=one-time) - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX diff --git a/docs/docs/partials/img/backup-header.png b/docs/docs/partials/img/backup-header.png index fbd133f32..629f13c8a 100644 Binary files a/docs/docs/partials/img/backup-header.png and b/docs/docs/partials/img/backup-header.png differ diff --git a/docs/docs/partials/img/sign-in-phone.jpeg b/docs/docs/partials/img/sign-in-phone.jpeg index 249216132..18fceed7d 100644 Binary files a/docs/docs/partials/img/sign-in-phone.jpeg and b/docs/docs/partials/img/sign-in-phone.jpeg differ diff --git a/docs/docs/partials/img/storage-template.png b/docs/docs/partials/img/storage-template.png index 1157a8642..5d829b862 100644 Binary files a/docs/docs/partials/img/storage-template.png and b/docs/docs/partials/img/storage-template.png differ diff --git a/docs/package-lock.json b/docs/package-lock.json index df2ff7ebf..c550a9deb 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -15,7 +15,7 @@ "@mdx-js/react": "^1.6.22", "autoprefixer": "^10.4.13", "classnames": "^2.3.2", - "clsx": "^1.2.1", + "clsx": "^2.0.0", "docusaurus-lunr-search": "^2.3.2", "docusaurus-preset-openapi": "^0.6.3", "postcss": "^8.4.25", @@ -28,7 +28,7 @@ "devDependencies": { "@docusaurus/module-type-aliases": "^2.4.1", "@tsconfig/docusaurus": "^1.0.5", - "prettier": "^2.8.8", + "prettier": "^3.0.0", "typescript": "^5.1.6" }, "engines": { @@ -2603,6 +2603,14 @@ "react-dom": "^16.8.4 || ^17.0.0" } }, + "node_modules/@docusaurus/theme-classic/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/@docusaurus/theme-common": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.4.3.tgz", @@ -2633,6 +2641,14 @@ "react-dom": "^16.8.4 || ^17.0.0" } }, + "node_modules/@docusaurus/theme-common/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/@docusaurus/theme-search-algolia": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.3.tgz", @@ -2663,6 +2679,14 @@ "react-dom": "^16.8.4 || ^17.0.0" } }, + "node_modules/@docusaurus/theme-search-algolia/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/@docusaurus/theme-translations": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.4.3.tgz", @@ -4948,9 +4972,9 @@ } }, "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", "engines": { "node": ">=6" } @@ -5995,6 +6019,14 @@ "react-dom": "^16.8.4 || ^17" } }, + "node_modules/docusaurus-lunr-search/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/docusaurus-plugin-openapi": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi/-/docusaurus-plugin-openapi-0.6.4.tgz", @@ -6025,6 +6057,14 @@ "react-dom": "^16.8.4 || ^17.0.0" } }, + "node_modules/docusaurus-plugin-openapi/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/docusaurus-plugin-openapi/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -6098,6 +6138,14 @@ "react-dom": "^16.8.4 || ^17.0.0" } }, + "node_modules/docusaurus-theme-openapi/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -10801,15 +10849,15 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -13232,19 +13280,19 @@ } }, "node_modules/tailwindcss": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", - "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", + "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.12", + "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.18.2", + "jiti": "^1.19.1", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -13566,9 +13614,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16745,6 +16793,13 @@ "rtlcss": "^3.5.0", "tslib": "^2.4.0", "utility-types": "^3.10.0" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } } }, "@docusaurus/theme-common": { @@ -16768,6 +16823,13 @@ "tslib": "^2.4.0", "use-sync-external-store": "^1.2.0", "utility-types": "^3.10.0" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } } }, "@docusaurus/theme-search-algolia": { @@ -16791,6 +16853,13 @@ "lodash": "^4.17.21", "tslib": "^2.4.0", "utility-types": "^3.10.0" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } } }, "@docusaurus/theme-translations": { @@ -18515,9 +18584,9 @@ } }, "clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==" }, "collapse-white-space": { "version": "1.0.6", @@ -19243,6 +19312,13 @@ "to-vfile": "^6.1.0", "unified": "^9.0.0", "unist-util-is": "^4.0.2" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } } }, "docusaurus-plugin-openapi": { @@ -19268,6 +19344,11 @@ "webpack": "^5.73.0" }, "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -19321,6 +19402,13 @@ "react-redux": "^7.2.0", "redux-devtools-extension": "^2.13.8", "webpack": "^5.73.0" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } } }, "dom-converter": { @@ -22663,9 +22751,9 @@ "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==" }, "prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true }, "pretty-error": { @@ -24517,19 +24605,19 @@ } }, "tailwindcss": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", - "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", + "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", "requires": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.12", + "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.18.2", + "jiti": "^1.19.1", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -24757,9 +24845,9 @@ } }, "typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==" + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==" }, "ua-parser-js": { "version": "1.0.36", diff --git a/docs/package.json b/docs/package.json index 19c972fbb..fa0b5b3e6 100644 --- a/docs/package.json +++ b/docs/package.json @@ -24,7 +24,7 @@ "@mdx-js/react": "^1.6.22", "autoprefixer": "^10.4.13", "classnames": "^2.3.2", - "clsx": "^1.2.1", + "clsx": "^2.0.0", "docusaurus-lunr-search": "^2.3.2", "docusaurus-preset-openapi": "^0.6.3", "postcss": "^8.4.25", @@ -37,7 +37,7 @@ "devDependencies": { "@docusaurus/module-type-aliases": "^2.4.1", "@tsconfig/docusaurus": "^1.0.5", - "prettier": "^2.8.8", + "prettier": "^3.0.0", "typescript": "^5.1.6" }, "browserslist": { diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index 72159f51c..e2251924f 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -34,7 +34,7 @@ function HomepageHeader() { - logo + screenshots
diff --git a/docs/src/pages/milestones.tsx b/docs/src/pages/milestones.tsx index 38dc091a9..6c7e3c585 100644 --- a/docs/src/pages/milestones.tsx +++ b/docs/src/pages/milestones.tsx @@ -3,6 +3,7 @@ import { mdiAndroid, mdiAppleIos, mdiArchiveOutline, + mdiBash, mdiBookSearchOutline, mdiCakeVariant, mdiCheckAll, @@ -15,6 +16,7 @@ import { mdiFile, mdiFileSearch, mdiFolder, + mdiForum, mdiHeart, mdiImage, mdiImageAlbum, @@ -41,6 +43,7 @@ import { mdiText, mdiThemeLightDark, mdiTrashCanOutline, + mdiVectorCombine, mdiVideo, mdiWeb, } from '@mdi/js'; @@ -49,6 +52,34 @@ import React from 'react'; import Timeline, { DateType, Item } from '../components/timeline'; const items: Item[] = [ + { + icon: mdiVectorCombine, + description: + 'The serving of the web app is merged into the server image, allowing us to remove two containers from the stack.', + title: 'Container consolidation', + release: 'v1.88.0', + tag: 'v1.88.0', + date: new Date(2023, 10, 20), + dateType: DateType.RELEASE, + }, + { + icon: mdiBash, + description: 'Version 2 of the Immich CLI is released, replacing the legacy v1 CLI.', + title: 'CLI v2', + release: 'v1.88.0', + tag: 'v1.88.0', + date: new Date(2023, 10, 19), + dateType: DateType.RELEASE, + }, + { + icon: mdiForum, + description: 'Comment a photo or a video in a shared album', + title: 'Activity', + release: 'v1.84.0', + tag: 'v1.84.0', + date: new Date(2023, 10, 1), + dateType: DateType.RELEASE, + }, { icon: mdiStar, description: 'Reach 20K Stars on GitHub!', diff --git a/docs/src/theme/SearchBar/algolia.css b/docs/src/theme/SearchBar/algolia.css index 8bea784b9..66e696e96 100644 --- a/docs/src/theme/SearchBar/algolia.css +++ b/docs/src/theme/SearchBar/algolia.css @@ -61,8 +61,12 @@ .searchbox__input { display: inline-block; box-sizing: border-box; - -webkit-transition: box-shadow 0.4s ease, background 0.4s ease; - transition: box-shadow 0.4s ease, background 0.4s ease; + -webkit-transition: + box-shadow 0.4s ease, + background 0.4s ease; + transition: + box-shadow 0.4s ease, + background 0.4s ease; border: 0; border-radius: 16px; box-shadow: inset 0 0 0 1px #cccccc; @@ -243,7 +247,9 @@ } .algolia-autocomplete .ds-dropdown-menu { - box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2), 0 2px 3px 0 rgba(0, 0, 0, 0.1); + box-shadow: + 0 1px 0 0 rgba(0, 0, 0, 0.2), + 0 2px 3px 0 rgba(0, 0, 0, 0.1); } @media (min-width: 601px) { diff --git a/docs/static/img/immich-screenshots.png b/docs/static/img/immich-screenshots.png index 7ad188e5a..c8569528d 100644 Binary files a/docs/static/img/immich-screenshots.png and b/docs/static/img/immich-screenshots.png differ diff --git a/docs/vercel.json b/docs/vercel.json index f973f913d..d05820ebe 100644 --- a/docs/vercel.json +++ b/docs/vercel.json @@ -12,7 +12,8 @@ { "source": "/docs/overview/logo-meaning", "destination": "/docs/overview/logo" }, { "source": "/docs/overview/technology-stack", "destination": "/docs/developer/architecture" }, { "source": "/docs/usage/automatic-backup", "destination": "/docs/features/automatic-backup" }, - { "source": "/docs/usage/bulk-upload", "destination": "/docs/features/bulk-upload" }, + { "source": "/docs/usage/bulk-upload", "destination": "/docs/features/command-line-interface" }, + { "source": "/docs/features/bulk-upload", "destination": "/docs/features/command-line-interface" }, { "source": "/docs/usage/oauth", "destination": "/docs/administration/oauth" }, { "source": "/docs/usage/post-installation", "destination": "/docs/install/post-install" }, { "source": "/docs/usage/update", "destination": "/docs/install/docker-compose#step-4---upgrading" }, diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index d43855320..62a7c86da 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-bookworm as builder +FROM python:3.11-bookworm@sha256:ba7a7ac30c38e119c4304f98ef0e188f90f4f67a958bb6899da9defb99bfb471 as builder ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ @@ -13,7 +13,7 @@ ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}" COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --only main -FROM python:3.11-slim-bookworm +FROM python:3.11-slim-bookworm@sha256:8f82989e563d0dbad057a874a96438a360978c148e34f36c1db8d2d61b5fd6f0 RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/* diff --git a/machine-learning/app/config.py b/machine-learning/app/config.py index f3b41d22d..fa4fefeb3 100644 --- a/machine-learning/app/config.py +++ b/machine-learning/app/config.py @@ -13,7 +13,8 @@ from .schemas import ModelType class Settings(BaseSettings): cache_folder: str = "/cache" - model_ttl: int = 0 + model_ttl: int = 300 + model_ttl_poll_s: int = 10 host: str = "0.0.0.0" port: int = 3003 workers: int = 1 @@ -38,8 +39,16 @@ class LogSettings(BaseSettings): _clean_name = str.maketrans(":\\/", "___", ".") +def clean_name(model_name: str) -> str: + return model_name.split("/")[-1].translate(_clean_name) + + def get_cache_dir(model_name: str, model_type: ModelType) -> Path: - return Path(settings.cache_folder) / model_type.value / model_name.translate(_clean_name) + return Path(settings.cache_folder) / model_type.value / clean_name(model_name) + + +def get_hf_model_name(model_name: str) -> str: + return f"immich-app/{clean_name(model_name)}" LOG_LEVELS: dict[str, int] = { diff --git a/machine-learning/app/conftest.py b/machine-learning/app/conftest.py index 3bbb89c52..5e2dc1e84 100644 --- a/machine-learning/app/conftest.py +++ b/machine-learning/app/conftest.py @@ -36,7 +36,8 @@ def deployed_app() -> TestClient: @pytest.fixture(scope="session") def responses() -> dict[str, Any]: - return json.load(open("responses.json", "r")) + responses: dict[str, Any] = json.load(open("responses.json", "r")) + return responses @pytest.fixture(scope="session") diff --git a/machine-learning/app/main.py b/machine-learning/app/main.py index 375c14a9e..bf232071b 100644 --- a/machine-learning/app/main.py +++ b/machine-learning/app/main.py @@ -1,5 +1,9 @@ import asyncio +import gc +import os +import sys import threading +import time from concurrent.futures import ThreadPoolExecutor from typing import Any from zipfile import BadZipFile @@ -7,7 +11,7 @@ from zipfile import BadZipFile import orjson from fastapi import FastAPI, Form, HTTPException, UploadFile from fastapi.responses import ORJSONResponse -from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile # type: ignore +from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile from starlette.formparsers import MultiPartParser from app.models.base import InferenceModel @@ -20,7 +24,7 @@ from .schemas import ( TextResponse, ) -MultiPartParser.max_file_size = 2**24 # spools to disk if payload is 16 MiB or larger +MultiPartParser.max_file_size = 2**26 # spools to disk if payload is 64 MiB or larger app = FastAPI() @@ -34,7 +38,10 @@ def init_state() -> None: ) # asyncio is a huge bottleneck for performance, so we use a thread pool to run blocking code app.state.thread_pool = ThreadPoolExecutor(settings.request_threads) if settings.request_threads > 0 else None - app.state.locks = {model_type: threading.Lock() for model_type in ModelType} + app.state.lock = threading.Lock() + app.state.last_called = None + if settings.model_ttl > 0 and settings.model_ttl_poll_s > 0: + asyncio.ensure_future(idle_shutdown_task()) log.info(f"Initialized request thread pool with {settings.request_threads} threads.") @@ -79,9 +86,9 @@ async def predict( async def run(model: InferenceModel, inputs: Any) -> Any: + app.state.last_called = time.time() if app.state.thread_pool is None: return model.predict(inputs) - return await asyncio.get_running_loop().run_in_executor(app.state.thread_pool, model.predict, inputs) @@ -90,7 +97,7 @@ async def load(model: InferenceModel) -> InferenceModel: return model def _load() -> None: - with app.state.locks[model.model_type]: + with app.state.lock: model.load() loop = asyncio.get_running_loop() @@ -113,3 +120,27 @@ async def load(model: InferenceModel) -> InferenceModel: else: await loop.run_in_executor(app.state.thread_pool, _load) return model + + +async def idle_shutdown_task() -> None: + while True: + log.debug("Checking for inactivity...") + if app.state.last_called is not None and time.time() - app.state.last_called > settings.model_ttl: + log.info("Shutting down due to inactivity.") + loop = asyncio.get_running_loop() + for task in asyncio.all_tasks(loop): + if task is not asyncio.current_task(): + try: + task.cancel() + except asyncio.CancelledError: + pass + sys.stderr.close() + sys.stdout.close() + sys.stdout = sys.stderr = open(os.devnull, "w") + try: + await app.state.model_cache.cache.clear() + gc.collect() + loop.stop() + except asyncio.CancelledError: + pass + await asyncio.sleep(settings.model_ttl_poll_s) diff --git a/machine-learning/app/models/__init__.py b/machine-learning/app/models/__init__.py index a8df0050d..fa00a8614 100644 --- a/machine-learning/app/models/__init__.py +++ b/machine-learning/app/models/__init__.py @@ -3,7 +3,8 @@ from typing import Any from app.schemas import ModelType from .base import InferenceModel -from .clip import MCLIPEncoder, OpenCLIPEncoder, is_mclip, is_openclip +from .clip import MCLIPEncoder, OpenCLIPEncoder +from .constants import is_insightface, is_mclip, is_openclip from .facial_recognition import FaceRecognizer from .image_classification import ImageClassifier @@ -15,11 +16,12 @@ def from_model_type(model_type: ModelType, model_name: str, **model_kwargs: Any) return OpenCLIPEncoder(model_name, **model_kwargs) elif is_mclip(model_name): return MCLIPEncoder(model_name, **model_kwargs) - else: - raise ValueError(f"Unknown CLIP model {model_name}") case ModelType.FACIAL_RECOGNITION: - return FaceRecognizer(model_name, **model_kwargs) + if is_insightface(model_name): + return FaceRecognizer(model_name, **model_kwargs) case ModelType.IMAGE_CLASSIFICATION: return ImageClassifier(model_name, **model_kwargs) case _: raise ValueError(f"Unknown model type {model_type}") + + raise ValueError(f"Unknown ${model_type} model {model_name}") diff --git a/machine-learning/app/models/base.py b/machine-learning/app/models/base.py index 4f597d876..d3252d000 100644 --- a/machine-learning/app/models/base.py +++ b/machine-learning/app/models/base.py @@ -7,8 +7,10 @@ from shutil import rmtree from typing import Any import onnxruntime as ort +from huggingface_hub import snapshot_download +from typing_extensions import Buffer -from ..config import get_cache_dir, log, settings +from ..config import get_cache_dir, get_hf_model_name, log, settings from ..schemas import ModelType @@ -78,9 +80,13 @@ class InferenceModel(ABC): def configure(self, **model_kwargs: Any) -> None: pass - @abstractmethod def _download(self) -> None: - ... + snapshot_download( + get_hf_model_name(self.model_name), + cache_dir=self.cache_dir, + local_dir=self.cache_dir, + local_dir_use_symlinks=False, + ) @abstractmethod def _load(self) -> None: @@ -134,11 +140,12 @@ class InferenceModel(ABC): # HF deep copies configs, so we need to make session options picklable -class PicklableSessionOptions(ort.SessionOptions): +class PicklableSessionOptions(ort.SessionOptions): # type: ignore[misc] def __getstate__(self) -> bytes: return pickle.dumps([(attr, getattr(self, attr)) for attr in dir(self) if not callable(getattr(self, attr))]) - def __setstate__(self, state: Any) -> None: - self.__init__() # type: ignore - for attr, val in pickle.loads(state): + def __setstate__(self, state: Buffer) -> None: + self.__init__() # type: ignore[misc] + attrs: list[tuple[str, Any]] = pickle.loads(state) + for attr, val in attrs: setattr(self, attr, val) diff --git a/machine-learning/app/models/cache.py b/machine-learning/app/models/cache.py index bd8b59b3e..1d6a0fc76 100644 --- a/machine-learning/app/models/cache.py +++ b/machine-learning/app/models/cache.py @@ -6,7 +6,7 @@ from aiocache.plugins import BasePlugin, TimingPlugin from app.models import from_model_type -from ..schemas import ModelType +from ..schemas import ModelType, has_profiling from .base import InferenceModel @@ -50,20 +50,20 @@ class ModelCache: key = f"{model_name}{model_type.value}{model_kwargs.get('mode', '')}" async with OptimisticLock(self.cache, key) as lock: - model = await self.cache.get(key) + model: InferenceModel | None = await self.cache.get(key) if model is None: model = from_model_type(model_type, model_name, **model_kwargs) await lock.cas(model, ttl=self.ttl) return model async def get_profiling(self) -> dict[str, float] | None: - if not hasattr(self.cache, "profiling"): + if not has_profiling(self.cache): return None - return self.cache.profiling # type: ignore + return self.cache.profiling -class RevalidationPlugin(BasePlugin): +class RevalidationPlugin(BasePlugin): # type: ignore[misc] """Revalidates cache item's TTL after cache hit.""" async def post_get( diff --git a/machine-learning/app/models/clip.py b/machine-learning/app/models/clip.py index da0381d3a..1dee967de 100644 --- a/machine-learning/app/models/clip.py +++ b/machine-learning/app/models/clip.py @@ -7,11 +7,10 @@ from typing import Any, Literal import numpy as np import onnxruntime as ort -from huggingface_hub import snapshot_download from PIL import Image from transformers import AutoTokenizer -from app.config import log +from app.config import clean_name, log from app.models.transforms import crop, get_pil_resampling, normalize, resize, to_numpy from app.schemas import ModelType, ndarray_f32, ndarray_i32, ndarray_i64 @@ -52,7 +51,7 @@ class BaseCLIPEncoder(InferenceModel): provider_options=self.provider_options, ) - def _predict(self, image_or_text: Image.Image | str) -> list[float]: + def _predict(self, image_or_text: Image.Image | str) -> ndarray_f32: if isinstance(image_or_text, bytes): image_or_text = Image.open(BytesIO(image_or_text)) @@ -61,16 +60,16 @@ class BaseCLIPEncoder(InferenceModel): if self.mode == "text": raise TypeError("Cannot encode image as text-only model") - outputs = self.vision_model.run(None, self.transform(image_or_text)) + outputs: ndarray_f32 = self.vision_model.run(None, self.transform(image_or_text))[0][0] case str(): if self.mode == "vision": raise TypeError("Cannot encode text as vision-only model") - outputs = self.text_model.run(None, self.tokenize(image_or_text)) + outputs = self.text_model.run(None, self.tokenize(image_or_text))[0][0] case _: raise TypeError(f"Expected Image or str, but got: {type(image_or_text)}") - return outputs[0][0].tolist() + return outputs @abstractmethod def tokenize(self, text: str) -> dict[str, ndarray_i32]: @@ -117,15 +116,7 @@ class OpenCLIPEncoder(BaseCLIPEncoder): mode: Literal["text", "vision"] | None = None, **model_kwargs: Any, ) -> None: - super().__init__(_clean_model_name(model_name), cache_dir, mode, **model_kwargs) - - def _download(self) -> None: - snapshot_download( - f"immich-app/{self.model_name}", - cache_dir=self.cache_dir, - local_dir=self.cache_dir, - local_dir_use_symlinks=False, - ) + super().__init__(clean_name(model_name), cache_dir, mode, **model_kwargs) def _load(self) -> None: super()._load() @@ -160,63 +151,16 @@ class OpenCLIPEncoder(BaseCLIPEncoder): @cached_property def model_cfg(self) -> dict[str, Any]: - return json.load(self.model_cfg_path.open()) + model_cfg: dict[str, Any] = json.load(self.model_cfg_path.open()) + return model_cfg @cached_property def preprocess_cfg(self) -> dict[str, Any]: - return json.load(self.preprocess_cfg_path.open()) + preprocess_cfg: dict[str, Any] = json.load(self.preprocess_cfg_path.open()) + return preprocess_cfg class MCLIPEncoder(OpenCLIPEncoder): def tokenize(self, text: str) -> dict[str, ndarray_i32]: tokens: dict[str, ndarray_i64] = self.tokenizer(text, return_tensors="np") return {k: v.astype(np.int32) for k, v in tokens.items()} - - -_OPENCLIP_MODELS = { - "RN50__openai", - "RN50__yfcc15m", - "RN50__cc12m", - "RN101__openai", - "RN101__yfcc15m", - "RN50x4__openai", - "RN50x16__openai", - "RN50x64__openai", - "ViT-B-32__openai", - "ViT-B-32__laion2b_e16", - "ViT-B-32__laion400m_e31", - "ViT-B-32__laion400m_e32", - "ViT-B-32__laion2b-s34b-b79k", - "ViT-B-16__openai", - "ViT-B-16__laion400m_e31", - "ViT-B-16__laion400m_e32", - "ViT-B-16-plus-240__laion400m_e31", - "ViT-B-16-plus-240__laion400m_e32", - "ViT-L-14__openai", - "ViT-L-14__laion400m_e31", - "ViT-L-14__laion400m_e32", - "ViT-L-14__laion2b-s32b-b82k", - "ViT-L-14-336__openai", - "ViT-H-14__laion2b-s32b-b79k", - "ViT-g-14__laion2b-s12b-b42k", -} - - -_MCLIP_MODELS = { - "LABSE-Vit-L-14", - "XLM-Roberta-Large-Vit-B-32", - "XLM-Roberta-Large-Vit-B-16Plus", - "XLM-Roberta-Large-Vit-L-14", -} - - -def _clean_model_name(model_name: str) -> str: - return model_name.split("/")[-1].replace("::", "__") - - -def is_openclip(model_name: str) -> bool: - return _clean_model_name(model_name) in _OPENCLIP_MODELS - - -def is_mclip(model_name: str) -> bool: - return _clean_model_name(model_name) in _MCLIP_MODELS diff --git a/machine-learning/app/models/constants.py b/machine-learning/app/models/constants.py new file mode 100644 index 000000000..53f3f3381 --- /dev/null +++ b/machine-learning/app/models/constants.py @@ -0,0 +1,57 @@ +from app.config import clean_name + +_OPENCLIP_MODELS = { + "RN50__openai", + "RN50__yfcc15m", + "RN50__cc12m", + "RN101__openai", + "RN101__yfcc15m", + "RN50x4__openai", + "RN50x16__openai", + "RN50x64__openai", + "ViT-B-32__openai", + "ViT-B-32__laion2b_e16", + "ViT-B-32__laion400m_e31", + "ViT-B-32__laion400m_e32", + "ViT-B-32__laion2b-s34b-b79k", + "ViT-B-16__openai", + "ViT-B-16__laion400m_e31", + "ViT-B-16__laion400m_e32", + "ViT-B-16-plus-240__laion400m_e31", + "ViT-B-16-plus-240__laion400m_e32", + "ViT-L-14__openai", + "ViT-L-14__laion400m_e31", + "ViT-L-14__laion400m_e32", + "ViT-L-14__laion2b-s32b-b82k", + "ViT-L-14-336__openai", + "ViT-H-14__laion2b-s32b-b79k", + "ViT-g-14__laion2b-s12b-b42k", +} + + +_MCLIP_MODELS = { + "LABSE-Vit-L-14", + "XLM-Roberta-Large-Vit-B-32", + "XLM-Roberta-Large-Vit-B-16Plus", + "XLM-Roberta-Large-Vit-L-14", +} + + +_INSIGHTFACE_MODELS = { + "antelopev2", + "buffalo_l", + "buffalo_m", + "buffalo_s", +} + + +def is_openclip(model_name: str) -> bool: + return clean_name(model_name) in _OPENCLIP_MODELS + + +def is_mclip(model_name: str) -> bool: + return clean_name(model_name) in _MCLIP_MODELS + + +def is_insightface(model_name: str) -> bool: + return clean_name(model_name) in _INSIGHTFACE_MODELS diff --git a/machine-learning/app/models/facial_recognition.py b/machine-learning/app/models/facial_recognition.py index 2ea7fdf67..24719eb83 100644 --- a/machine-learning/app/models/facial_recognition.py +++ b/machine-learning/app/models/facial_recognition.py @@ -1,4 +1,3 @@ -import zipfile from pathlib import Path from typing import Any @@ -7,9 +6,9 @@ import numpy as np import onnxruntime as ort from insightface.model_zoo import ArcFaceONNX, RetinaFace from insightface.utils.face_align import norm_crop -from insightface.utils.storage import BASE_REPO_URL, download_file -from app.schemas import ModelType, ndarray_f32 +from app.config import clean_name +from app.schemas import BoundingBox, Face, ModelType, ndarray_f32 from .base import InferenceModel @@ -25,37 +24,21 @@ class FaceRecognizer(InferenceModel): **model_kwargs: Any, ) -> None: self.min_score = model_kwargs.pop("minScore", min_score) - super().__init__(model_name, cache_dir, **model_kwargs) - - def _download(self) -> None: - zip_file = self.cache_dir / f"{self.model_name}.zip" - download_file(f"{BASE_REPO_URL}/{self.model_name}.zip", zip_file) - with zipfile.ZipFile(zip_file, "r") as zip: - members = zip.namelist() - det_file = next(model for model in members if model.startswith("det_")) - rec_file = next(model for model in members if model.startswith("w600k_")) - zip.extractall(self.cache_dir, members=[det_file, rec_file]) - zip_file.unlink() + super().__init__(clean_name(model_name), cache_dir, **model_kwargs) def _load(self) -> None: - try: - det_file = next(self.cache_dir.glob("det_*.onnx")) - rec_file = next(self.cache_dir.glob("w600k_*.onnx")) - except StopIteration: - raise FileNotFoundError("Facial recognition models not found in cache directory") - self.det_model = RetinaFace( session=ort.InferenceSession( - det_file.as_posix(), + self.det_file.as_posix(), sess_options=self.sess_options, providers=self.providers, provider_options=self.provider_options, ), ) self.rec_model = ArcFaceONNX( - rec_file.as_posix(), + self.rec_file.as_posix(), session=ort.InferenceSession( - rec_file.as_posix(), + self.rec_file.as_posix(), sess_options=self.sess_options, providers=self.providers, provider_options=self.provider_options, @@ -69,7 +52,7 @@ class FaceRecognizer(InferenceModel): ) self.rec_model.prepare(ctx_id=0) - def _predict(self, image: ndarray_f32 | bytes) -> list[dict[str, Any]]: + def _predict(self, image: ndarray_f32 | bytes) -> list[Face]: if isinstance(image, bytes): image = cv2.imdecode(np.frombuffer(image, np.uint8), cv2.IMREAD_COLOR) bboxes, kpss = self.det_model.detect(image) @@ -84,26 +67,33 @@ class FaceRecognizer(InferenceModel): height, width, _ = image.shape for (x1, y1, x2, y2), score, kps in zip(bboxes, scores, kpss): cropped_img = norm_crop(image, kps) - embedding = self.rec_model.get_feat(cropped_img)[0].tolist() - results.append( - { - "imageWidth": width, - "imageHeight": height, - "boundingBox": { - "x1": x1, - "y1": y1, - "x2": x2, - "y2": y2, - }, - "score": score, - "embedding": embedding, - } - ) + embedding: ndarray_f32 = self.rec_model.get_feat(cropped_img)[0] + face: Face = { + "imageWidth": width, + "imageHeight": height, + "boundingBox": { + "x1": x1, + "y1": y1, + "x2": x2, + "y2": y2, + }, + "score": score, + "embedding": embedding, + } + results.append(face) return results @property def cached(self) -> bool: - return self.cache_dir.is_dir() and any(self.cache_dir.glob("*.onnx")) + return self.det_file.is_file() and self.rec_file.is_file() + + @property + def det_file(self) -> Path: + return self.cache_dir / "detection" / "model.onnx" + + @property + def rec_file(self) -> Path: + return self.cache_dir / "recognition" / "model.onnx" def configure(self, **model_kwargs: Any) -> None: self.det_model.det_thresh = model_kwargs.pop("minScore", self.det_model.det_thresh) diff --git a/machine-learning/app/models/image_classification.py b/machine-learning/app/models/image_classification.py index cbf784e5a..b8c38327c 100644 --- a/machine-learning/app/models/image_classification.py +++ b/machine-learning/app/models/image_classification.py @@ -66,7 +66,7 @@ class ImageClassifier(InferenceModel): def _predict(self, image: Image.Image | bytes) -> list[str]: if isinstance(image, bytes): image = Image.open(BytesIO(image)) - predictions: list[dict[str, Any]] = self.model(image) # type: ignore + predictions: list[dict[str, Any]] = self.model(image) tags = [tag for pred in predictions for tag in pred["label"].split(", ") if pred["score"] >= self.min_score] return tags diff --git a/machine-learning/app/schemas.py b/machine-learning/app/schemas.py index ad1faac8c..9e7f62fc8 100644 --- a/machine-learning/app/schemas.py +++ b/machine-learning/app/schemas.py @@ -1,17 +1,12 @@ from enum import StrEnum -from typing import TypeAlias +from typing import Any, Protocol, TypeAlias, TypedDict, TypeGuard import numpy as np from pydantic import BaseModel - -def to_lower_camel(string: str) -> str: - tokens = [token.capitalize() if i > 0 else token for i, token in enumerate(string.split("_"))] - return "".join(tokens) - - -class TextModelRequest(BaseModel): - text: str +ndarray_f32: TypeAlias = np.ndarray[int, np.dtype[np.float32]] +ndarray_i64: TypeAlias = np.ndarray[int, np.dtype[np.int64]] +ndarray_i32: TypeAlias = np.ndarray[int, np.dtype[np.int32]] class TextResponse(BaseModel): @@ -22,7 +17,7 @@ class MessageResponse(BaseModel): message: str -class BoundingBox(BaseModel): +class BoundingBox(TypedDict): x1: int y1: int x2: int @@ -35,6 +30,17 @@ class ModelType(StrEnum): FACIAL_RECOGNITION = "facial-recognition" -ndarray_f32: TypeAlias = np.ndarray[int, np.dtype[np.float32]] -ndarray_i64: TypeAlias = np.ndarray[int, np.dtype[np.int64]] -ndarray_i32: TypeAlias = np.ndarray[int, np.dtype[np.int32]] +class HasProfiling(Protocol): + profiling: dict[str, float] + + +class Face(TypedDict): + boundingBox: BoundingBox + embedding: ndarray_f32 + imageWidth: int + imageHeight: int + score: float + + +def has_profiling(obj: Any) -> TypeGuard[HasProfiling]: + return hasattr(obj, "profiling") and type(obj.profiling) == dict diff --git a/machine-learning/app/test_main.py b/machine-learning/app/test_main.py index 0b28f8234..318c3b045 100644 --- a/machine-learning/app/test_main.py +++ b/machine-learning/app/test_main.py @@ -75,9 +75,9 @@ class TestCLIP: embedding = clip_encoder.predict(pil_image) assert clip_encoder.mode == "vision" - assert isinstance(embedding, list) - assert len(embedding) == clip_model_cfg["embed_dim"] - assert all([isinstance(num, float) for num in embedding]) + assert isinstance(embedding, np.ndarray) + assert embedding.shape[0] == clip_model_cfg["embed_dim"] + assert embedding.dtype == np.float32 clip_encoder.vision_model.run.assert_called_once() def test_basic_text( @@ -97,22 +97,22 @@ class TestCLIP: embedding = clip_encoder.predict("test search query") assert clip_encoder.mode == "text" - assert isinstance(embedding, list) - assert len(embedding) == clip_model_cfg["embed_dim"] - assert all([isinstance(num, float) for num in embedding]) + assert isinstance(embedding, np.ndarray) + assert embedding.shape[0] == clip_model_cfg["embed_dim"] + assert embedding.dtype == np.float32 clip_encoder.text_model.run.assert_called_once() class TestFaceRecognition: def test_set_min_score(self, mocker: MockerFixture) -> None: mocker.patch.object(FaceRecognizer, "load") - face_recognizer = FaceRecognizer("test_model_name", cache_dir="test_cache", min_score=0.5) + face_recognizer = FaceRecognizer("buffalo_s", cache_dir="test_cache", min_score=0.5) assert face_recognizer.min_score == 0.5 def test_basic(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None: mocker.patch.object(FaceRecognizer, "load") - face_recognizer = FaceRecognizer("test_model_name", min_score=0.0, cache_dir="test_cache") + face_recognizer = FaceRecognizer("buffalo_s", min_score=0.0, cache_dir="test_cache") det_model = mock.Mock() num_faces = 2 @@ -133,9 +133,9 @@ class TestFaceRecognition: for face in faces: assert face["imageHeight"] == 800 assert face["imageWidth"] == 600 - assert isinstance(face["embedding"], list) - assert len(face["embedding"]) == 512 - assert all([isinstance(num, float) for num in face["embedding"]]) + assert isinstance(face["embedding"], np.ndarray) + assert face["embedding"].shape[0] == 512 + assert face["embedding"].dtype == np.float32 det_model.detect.assert_called_once() assert rec_model.get_feat.call_count == num_faces diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index 1c74546bc..7bd04e0c2 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim as builder +FROM mambaorg/micromamba:bookworm-slim@sha256:d20c621f3ae42f50f380166b15b6c88b14fa62ab6ea188f2cef33451d64057c7 as builder ENV NODE_ENV=production \ TRANSFORMERS_CACHE=/cache \ diff --git a/machine-learning/export/models/openclip.py b/machine-learning/export/models/openclip.py index c29dafce7..46c11cb4e 100644 --- a/machine-learning/export/models/openclip.py +++ b/machine-learning/export/models/openclip.py @@ -1,6 +1,7 @@ import tempfile import warnings from dataclasses import dataclass, field +from math import e from pathlib import Path import open_clip @@ -69,10 +70,12 @@ def export_image_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, output_path = Path(output_path) def encode_image(image: torch.Tensor) -> torch.Tensor: - return model.encode_image(image, normalize=True) + output = model.encode_image(image, normalize=True) + assert isinstance(output, torch.Tensor) + return output args = (torch.randn(1, 3, model_cfg.image_size, model_cfg.image_size),) - traced = torch.jit.trace(encode_image, args) + traced = torch.jit.trace(encode_image, args) # type: ignore[no-untyped-call] with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) @@ -91,10 +94,12 @@ def export_text_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, o output_path = Path(output_path) def encode_text(text: torch.Tensor) -> torch.Tensor: - return model.encode_text(text, normalize=True) + output = model.encode_text(text, normalize=True) + assert isinstance(output, torch.Tensor) + return output args = (torch.ones(1, model_cfg.sequence_length, dtype=torch.int32),) - traced = torch.jit.trace(encode_text, args) + traced = torch.jit.trace(encode_text, args) # type: ignore[no-untyped-call] with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 2b5a11e8b..e9ec397ff 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -585,68 +585,6 @@ files = [ test = ["PyYAML", "mock", "pytest"] yaml = ["PyYAML"] -[[package]] -name = "contourpy" -version = "1.1.0" -description = "Python library for calculating contours of 2D quadrilateral grids" -optional = false -python-versions = ">=3.8" -files = [ - {file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"}, - {file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"}, - {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"}, - {file = "contourpy-1.1.0-cp310-cp310-win32.whl", hash = "sha256:9b2dd2ca3ac561aceef4c7c13ba654aaa404cf885b187427760d7f7d4c57cff8"}, - {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"}, - {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"}, - {file = "contourpy-1.1.0-cp311-cp311-win32.whl", hash = "sha256:edb989d31065b1acef3828a3688f88b2abb799a7db891c9e282df5ec7e46221b"}, - {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"}, - {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"}, - {file = "contourpy-1.1.0-cp38-cp38-win32.whl", hash = "sha256:108dfb5b3e731046a96c60bdc46a1a0ebee0760418951abecbe0fc07b5b93b27"}, - {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"}, - {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"}, - {file = "contourpy-1.1.0-cp39-cp39-win32.whl", hash = "sha256:71551f9520f008b2950bef5f16b0e3587506ef4f23c734b71ffb7b89f8721999"}, - {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"}, - {file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"}, -] - -[package.dependencies] -numpy = ">=1.16" - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "wurlitzer"] - [[package]] name = "contourpy" version = "1.1.1" @@ -2408,35 +2346,35 @@ reference = ["Pillow", "google-re2"] [[package]] name = "onnxruntime" -version = "1.16.1" +version = "1.16.2" description = "ONNX Runtime is a runtime accelerator for Machine Learning models" optional = false python-versions = "*" files = [ - {file = "onnxruntime-1.16.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:28b2c7f444b4119950b69370801cd66067f403d19cbaf2a444735d7c269cce4a"}, - {file = "onnxruntime-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c24e04f33e7899f6aebb03ed51e51d346c1f906b05c5569d58ac9a12d38a2f58"}, - {file = "onnxruntime-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fa93b166f2d97063dc9f33c5118c5729a4a5dd5617296b6dbef42f9047b3e81"}, - {file = "onnxruntime-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:042dd9201b3016ee18f8f8bc4609baf11ff34ca1ff489c0a46bcd30919bf883d"}, - {file = "onnxruntime-1.16.1-cp310-cp310-win32.whl", hash = "sha256:c20aa0591f305012f1b21aad607ed96917c86ae7aede4a4dd95824b3d124ceb7"}, - {file = "onnxruntime-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:5581873e578917bea76d6434ee7337e28195d03488dcf72d161d08e9398c6249"}, - {file = "onnxruntime-1.16.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:ef8c0c8abf5f309aa1caf35941380839dc5f7a2fa53da533be4a3f254993f120"}, - {file = "onnxruntime-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e680380bea35a137cbc3efd67a17486e96972901192ad3026ee79c8d8fe264f7"}, - {file = "onnxruntime-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e62cc38ce1a669013d0a596d984762dc9c67c56f60ecfeee0d5ad36da5863f6"}, - {file = "onnxruntime-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:025c7a4d57bd2e63b8a0f84ad3df53e419e3df1cc72d63184f2aae807b17c13c"}, - {file = "onnxruntime-1.16.1-cp311-cp311-win32.whl", hash = "sha256:9ad074057fa8d028df248b5668514088cb0937b6ac5954073b7fb9b2891ffc8c"}, - {file = "onnxruntime-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:d5e43a3478bffc01f817ecf826de7b25a2ca1bca8547d70888594ab80a77ad24"}, - {file = "onnxruntime-1.16.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3aef4d70b0930e29a8943eab248cd1565664458d3a62b2276bd11181f28fd0a3"}, - {file = "onnxruntime-1.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:55a7b843a57c8ca0c8ff169428137958146081d5d76f1a6dd444c4ffcd37c3c2"}, - {file = "onnxruntime-1.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c631af1941bf3b5f7d063d24c04aacce8cff0794e157c497e315e89ac5ad7b"}, - {file = "onnxruntime-1.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671f296c3d5c233f601e97a10ab5a1dd8e65ba35c7b7b0c253332aba9dff330"}, - {file = "onnxruntime-1.16.1-cp38-cp38-win32.whl", hash = "sha256:eb3802305023dd05e16848d4e22b41f8147247894309c0c27122aaa08793b3d2"}, - {file = "onnxruntime-1.16.1-cp38-cp38-win_amd64.whl", hash = "sha256:fecfb07443d09d271b1487f401fbdf1ba0c829af6fd4fe8f6af25f71190e7eb9"}, - {file = "onnxruntime-1.16.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:de3e12094234db6545c67adbf801874b4eb91e9f299bda34c62967ef0050960f"}, - {file = "onnxruntime-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ff723c2a5621b5e7103f3be84d5aae1e03a20621e72219dddceae81f65f240af"}, - {file = "onnxruntime-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14a7fb3073aaf6b462e3d7fb433320f7700558a8892e5021780522dc4574292a"}, - {file = "onnxruntime-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:963159f1f699b0454cd72fcef3276c8a1aab9389a7b301bcd8e320fb9d9e8597"}, - {file = "onnxruntime-1.16.1-cp39-cp39-win32.whl", hash = "sha256:85771adb75190db9364b25ddec353ebf07635b83eb94b64ed014f1f6d57a3857"}, - {file = "onnxruntime-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:d32d2b30799c1f950123c60ae8390818381fd5f88bdf3627eeca10071c155dc5"}, + {file = "onnxruntime-1.16.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:e19316bb15c29ca0397e78861ee7cdb4db763ac5c53eaa83169bcdcb1149878c"}, + {file = "onnxruntime-1.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:773f6d99d1e6a58936a55a4933c66674241dace9ec4bab71664cdfa170a7cd87"}, + {file = "onnxruntime-1.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b8df9583a6e874f1983b85a361d22c205c96e926626eb486d3e69d72642f79"}, + {file = "onnxruntime-1.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceef600de846997e3ef5f9af956ae87c88d84d6e925c3e9d435ce17ea223568f"}, + {file = "onnxruntime-1.16.2-cp310-cp310-win32.whl", hash = "sha256:4fed41edb766c6adea6c34f1eb63a344d697fd4625133e5e48f23950bce60803"}, + {file = "onnxruntime-1.16.2-cp310-cp310-win_amd64.whl", hash = "sha256:9fc410ec220804fb384e7cb4fd68c474d89da11a1b68184db2001d64ba1477a9"}, + {file = "onnxruntime-1.16.2-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:aa09d8d9d9a4dc2f6647b5135bb540da36e2d78206aaf14140ba73e05928c4f8"}, + {file = "onnxruntime-1.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68f8d3347f11fcc6256266c562e4314b8c6da3e30fc275052a2ab693540b17fd"}, + {file = "onnxruntime-1.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16217fa87d3482300a91036f9b499c85215a3b495de1ef9a68cbcf3df1a7c548"}, + {file = "onnxruntime-1.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6b7046005442fcd09b86647bdc9a85d60c1367cb36ce7f16b942744cf27fe4"}, + {file = "onnxruntime-1.16.2-cp311-cp311-win32.whl", hash = "sha256:773c231e526f815b8a3f3549d216cd8fed4c9e226e9e16e86af1b69a4bd29b58"}, + {file = "onnxruntime-1.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:90e83a93b3d946c4a1d9dcbae286350accb0d80512d7c1b85953a444d19c0058"}, + {file = "onnxruntime-1.16.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:8616f56905775dd8beeae11cf145542fff06c38cd97bfe9afe0c4a66142fc6d5"}, + {file = "onnxruntime-1.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9f5e1d5ca5560044896edb2ad79113f863dc7daa804a26787c7b21c2a96d41e7"}, + {file = "onnxruntime-1.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97ce538ffb668c4897e7500a586c150a045869876e0234e0611c4e4f428be63"}, + {file = "onnxruntime-1.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cadf175baa782599f36586c23f84fe12b02702ceb59be57dbd8eefc6cc13cc4"}, + {file = "onnxruntime-1.16.2-cp38-cp38-win32.whl", hash = "sha256:0ffd3b8a3039be713476b8783d254564976664c9b51ec70e7fb5d3e2832bf0f0"}, + {file = "onnxruntime-1.16.2-cp38-cp38-win_amd64.whl", hash = "sha256:e2211f336e83819edbf174dcf56de35b0dcbfc6c92d3b685c8d85fba19bdf97d"}, + {file = "onnxruntime-1.16.2-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:98a49bda980bcf819f8d9be880e3e7ba8a1df66aa5ce4fc7bb68ba9acf1fc7ad"}, + {file = "onnxruntime-1.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1f1e90fa0f43e988cd043e5a4b1eb77eda6cbd7523f316d93d36b33ff1ceb91f"}, + {file = "onnxruntime-1.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0cbdb7df8078b2e8d9804de948963961eb8c6f417ef35ed243455162a9a065c"}, + {file = "onnxruntime-1.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93c1cbd885c5fe0018b982c9dabe3cc3531416a3b50d0958a291605b32fe3ce"}, + {file = "onnxruntime-1.16.2-cp39-cp39-win32.whl", hash = "sha256:713101b65d74438f380f5ea2475ce4f6026171e6229100e5be2baa92519fca17"}, + {file = "onnxruntime-1.16.2-cp39-cp39-win_amd64.whl", hash = "sha256:3382934f9d86060b6bacd3eb4633c5ff904be2c99d3a7fb7faf2828381b15928"}, ] [package.dependencies] @@ -2578,64 +2516,6 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] -[[package]] -name = "pandas" -version = "2.1.0" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pandas-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40dd20439ff94f1b2ed55b393ecee9cb6f3b08104c2c40b0cb7186a2f0046242"}, - {file = "pandas-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f38e4fedeba580285eaac7ede4f686c6701a9e618d8a857b138a126d067f2f"}, - {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e6a0fe052cf27ceb29be9429428b4918f3740e37ff185658f40d8702f0b3e09"}, - {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d81e1813191070440d4c7a413cb673052b3b4a984ffd86b8dd468c45742d3cc"}, - {file = "pandas-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb20252720b1cc1b7d0b2879ffc7e0542dd568f24d7c4b2347cb035206936421"}, - {file = "pandas-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:38f74ef7ebc0ffb43b3d633e23d74882bce7e27bfa09607f3c5d3e03ffd9a4a5"}, - {file = "pandas-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cda72cc8c4761c8f1d97b169661f23a86b16fdb240bdc341173aee17e4d6cedd"}, - {file = "pandas-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d97daeac0db8c993420b10da4f5f5b39b01fc9ca689a17844e07c0a35ac96b4b"}, - {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8c58b1113892e0c8078f006a167cc210a92bdae23322bb4614f2f0b7a4b510f"}, - {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:629124923bcf798965b054a540f9ccdfd60f71361255c81fa1ecd94a904b9dd3"}, - {file = "pandas-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:70cf866af3ab346a10debba8ea78077cf3a8cd14bd5e4bed3d41555a3280041c"}, - {file = "pandas-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d53c8c1001f6a192ff1de1efe03b31a423d0eee2e9e855e69d004308e046e694"}, - {file = "pandas-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86f100b3876b8c6d1a2c66207288ead435dc71041ee4aea789e55ef0e06408cb"}, - {file = "pandas-2.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28f330845ad21c11db51e02d8d69acc9035edfd1116926ff7245c7215db57957"}, - {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9a6ccf0963db88f9b12df6720e55f337447aea217f426a22d71f4213a3099a6"}, - {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99e678180bc59b0c9443314297bddce4ad35727a1a2656dbe585fd78710b3b9"}, - {file = "pandas-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b31da36d376d50a1a492efb18097b9101bdbd8b3fbb3f49006e02d4495d4c644"}, - {file = "pandas-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0164b85937707ec7f70b34a6c3a578dbf0f50787f910f21ca3b26a7fd3363437"}, - {file = "pandas-2.1.0.tar.gz", hash = "sha256:62c24c7fc59e42b775ce0679cfa7b14a5f9bfb7643cfbe708c960699e05fb918"}, -] - -[package.dependencies] -numpy = {version = ">=1.23.2", markers = "python_version >= \"3.11\""} -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.1" - -[package.extras] -all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] -aws = ["s3fs (>=2022.05.0)"] -clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] -compression = ["zstandard (>=0.17.0)"] -computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] -consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] -feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2022.05.0)"] -gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] -hdf5 = ["tables (>=3.7.0)"] -html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] -mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] -parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] -plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] -spss = ["pyreadstat (>=1.1.5)"] -sql-other = ["SQLAlchemy (>=1.4.36)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.8.0)"] - [[package]] name = "pandas" version = "2.1.2" @@ -4771,5 +4651,5 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" -python-versions = "^3.11" -content-hash = "bba5f87aa67bc1d2283a9f4b471ef78e572337f22413870d324e908014410d53" +python-versions = "~3.11" +content-hash = "a4c9b3550bb2a67a54b9ab70e700b24fb9eb0b652e90d7dd8ec92abd121ca6e3" diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 6fea1e5d5..b7c831429 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,13 +1,13 @@ [tool.poetry] name = "machine-learning" -version = "1.84.0" +version = "1.89.0" description = "" authors = ["Hau Tran "] readme = "README.md" packages = [{include = "app"}] [tool.poetry.dependencies] -python = "^3.11" +python = "~3.11" torch = [ {markers = "platform_machine == 'arm64' or platform_machine == 'aarch64'", version = "=2.1.0", source = "pypi"}, {markers = "platform_machine == 'amd64' or platform_machine == 'x86_64'", version = "=2.1.0", source = "pytorch-cpu"} diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index b570c2b2e..230e2a0d7 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -36,3 +36,57 @@ analyzer: - openapi/ - openapi/test/ - lib/generated_plugin_registrant.dart + +plugins: + - custom_lint + +dart_code_metrics: + metrics: + cyclomatic-complexity: 20 + number-of-parameters: 4 + maximum-nesting-level: 5 + rules: + # Common + - avoid-accessing-collections-by-constant-index + - avoid-accessing-other-classes-private-members + - avoid-cascade-after-if-null + - avoid-collapsible-if + - avoid-collection-methods-with-unrelated-types + - avoid-declaring-call-method + - avoid-double-slash-imports + - avoid-duplicate-cascades + - avoid-duplicate-patterns + - avoid-generics-shadowing + - avoid-global-state + # Flutter + - add-copy-with: + file-name-pattern: '.model.dart' + - always-remove-listener + - avoid-border-all + - avoid-empty-setstate + - avoid-expanded-as-spacer + - avoid-incomplete-copy-with + - avoid-inherited-widget-in-initstate + - avoid-late-context + - avoid-recursive-widget-calls + - avoid-returning-widgets + - avoid-shrink-wrap-in-lists + - avoid-single-child-column-or-row + - avoid-state-constructors + - avoid-stateless-widget-initialized-fields + - avoid-unnecessary-overrides-in-state + - avoid-unnecessary-stateful-widgets + - avoid-wrapping-in-padding + - dispose-fields + - prefer-const-border-radius + - prefer-correct-edge-insets-constructor + - prefer-dedicated-media-query-methods + - prefer-define-hero-tag + - prefer-extracting-callbacks + - prefer-single-widget-per-file: + ignore-private-widgets: true + - prefer-sliver-prefix + - prefer-text-rich + - prefer-using-list-view + - proper-super-calls + - use-setstate-synchronously diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 281fa52d2..d68edc784 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" => 108, - "android.injected.version.name" => "1.84.0", + "android.injected.version.code" => 113, + "android.injected.version.name" => "1.89.0", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index a0530d9aa..3e8a519ff 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/assets/i18n/ca.json b/mobile/assets/i18n/ca.json index 1ed97ed07..36aad1957 100644 --- a/mobile/assets/i18n/ca.json +++ b/mobile/assets/i18n/ca.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Error al modificar el títol de l'àlbum", "album_viewer_appbar_share_leave": "Surt de l'àlbum", "album_viewer_appbar_share_remove": "Treu de l'àlbum", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Afegeix usuaris", "all_people_page_title": "Persones", "all_videos_page_title": "Vídeos", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No s'ha trobat res arxivat", "archive_page_title": "Arxiu({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Ús de memòria cau", "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Configuració de la memòria cau", "change_password_form_confirm_password": "Confirma la contrasenya", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", @@ -164,10 +170,15 @@ "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", "home_page_add_to_album_success": "Added {added} assets to album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Can not archive local assets yet, skipping", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Building the timeline", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "image_viewer_page_state_provider_download_error": "Download Error", "image_viewer_page_state_provider_download_success": "Download Success", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", "partner_page_title": "Company", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Get started", "permission_onboarding_go_to_settings": "Go to settings", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "profile_drawer_app_logs": "Logs", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Settings", "profile_drawer_sign_out": "Tanca la sessió", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Error al crear l'àlbum", "select_user_for_sharing_page_share_suggestions": "Suggestions", "server_info_box_app_version": "Versió de l'aplicació", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "Versió del servidor", "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", @@ -300,6 +315,12 @@ "share_add_photos": "Afegeix fotografies", "share_add_title": "Afegeix un títol", "share_create_album": "Crea un àlbum", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Preparing...", "shared_link_app_bar_title": "Shared Links", "shared_link_create_app_bar_title": "Create link to share", @@ -311,6 +332,9 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", diff --git a/mobile/assets/i18n/cs-CZ.json b/mobile/assets/i18n/cs-CZ.json index d7151e9cf..8290c8f8c 100644 --- a/mobile/assets/i18n/cs-CZ.json +++ b/mobile/assets/i18n/cs-CZ.json @@ -13,7 +13,7 @@ "album_info_card_backup_album_included": "ZAHRNUTO", "album_thumbnail_card_item": "1 položka", "album_thumbnail_card_items": "{} položek", - "album_thumbnail_card_shared": "Sdíleno", + "album_thumbnail_card_shared": " · Sdíleno", "album_thumbnail_owned": "Vlastní", "album_thumbnail_shared_by": "Sdílel(a) {}", "album_viewer_appbar_share_delete": "Odstranit album", @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Nepodařilo se změnit název alba", "album_viewer_appbar_share_leave": "Opustit album", "album_viewer_appbar_share_remove": "Odstranit z alba", + "album_viewer_appbar_share_to": "Sdílet na", "album_viewer_page_share_add_users": "Přidat uživatele", "all_people_page_title": "Lidé", "all_videos_page_title": "Videa", + "app_bar_signout_dialog_content": "Určitě se chcete odhlásit?", + "app_bar_signout_dialog_ok": "Ano", + "app_bar_signout_dialog_title": "Odhlásit se", "archive_page_no_archived_assets": "Nebyla nalezena žádná archivovaná média", "archive_page_title": "Archív ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamické rozložení", @@ -44,11 +48,11 @@ "backup_all": "Vše", "backup_background_service_backup_failed_message": "Zálohování médií selhalo. Zkouším to znovu...", "backup_background_service_connection_failed_message": "Nepodařilo se připojit k serveru. Zkouším to znovu...", - "backup_background_service_current_upload_notification": "Zálohování {}", + "backup_background_service_current_upload_notification": "Nahrávání {}", "backup_background_service_default_notification": "Kontrola nových médií…", "backup_background_service_error_title": "Chyba zálohování", "backup_background_service_in_progress_notification": "Zálohování vašich médií...", - "backup_background_service_upload_failure_notification": "Nepodařilo se zálohovat {}", + "backup_background_service_upload_failure_notification": "Nepodařilo se nahrát {}", "backup_controller_page_albums": "Zálohovaná alba", "backup_controller_page_background_app_refresh_disabled_content": "Povolte obnovení aplikace na pozadí v Nastavení > Obecné > Obnovení aplikace na pozadí, abyste mohli používat zálohování na pozadí.", "backup_controller_page_background_app_refresh_disabled_title": " Obnovování aplikací na pozadí je vypnuté", @@ -91,14 +95,14 @@ "backup_controller_page_total_sub": "Všechny jedinečné fotografie a videa z vybraných alb", "backup_controller_page_turn_off": "Vypnout zálohování na popředí", "backup_controller_page_turn_on": "Povolit zálohování na popředí", - "backup_controller_page_uploading_file_info": "Informace o zálohovaném souboru", + "backup_controller_page_uploading_file_info": "Informace o nahraném souboru", "backup_err_only_album": "Nelze odstranit jediné vybrané album", "backup_info_card_assets": "položek", "backup_manual_cancelled": "Zrušeno", "backup_manual_failed": "Selhalo", - "backup_manual_in_progress": "Zálohování již probíhá. Zkuste to znovu později", + "backup_manual_in_progress": "Nahrávání již probíhá. Zkuste to znovu později", "backup_manual_success": "Úspěch", - "backup_manual_title": "Stav zálohování", + "backup_manual_title": "Stav nahrávání", "cache_settings_album_thumbnails": "Náhledy stránek knihovny (položek {})", "cache_settings_clear_cache_button": "Vymazat vyrovnávací paměť", "cache_settings_clear_cache_button_title": "Vymaže vyrovnávací paměť aplikace. To výrazně ovlivní výkon aplikace, dokud se vyrovnávací paměť neobnoví.", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Použití vyrovnávací paměti", "cache_settings_subtitle": "Ovládání chování mobilní aplikace Immich v mezipaměti", "cache_settings_thumbnail_size": "Velikost vyrovnávací paměti náhledů (položek {})", + "cache_settings_tile_subtitle": "Ovládání chování místního úložiště", + "cache_settings_tile_title": "Místní úložiště", "cache_settings_title": "Nastavení vyrovnávací paměti", "change_password_form_confirm_password": "Potvrďte heslo", - "change_password_form_description": "Dobrý den, {firstName} {lastName},\n\nje to buď poprvé, co se přihlašujete do systému, nebo byl vytvořen požadavek na změnu hesla. Níže zadejte nové heslo.", + "change_password_form_description": "Dobrý den, {name},\n\nje to buď poprvé, co se přihlašujete do systému, nebo byl vytvořen požadavek na změnu hesla. Níže zadejte nové heslo.", "change_password_form_new_password": "Nové heslo", "change_password_form_password_mismatch": "Hesla se neshodují", "change_password_form_reenter_new_password": "Znovu zadejte nové heslo", @@ -130,10 +136,10 @@ "control_bottom_app_bar_delete": "Vymazat", "control_bottom_app_bar_favorite": "Oblíbené", "control_bottom_app_bar_share": "Sdílet", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_share_to": "Sdílet v", + "control_bottom_app_bar_stack": "Zásobník", "control_bottom_app_bar_unarchive": "Odarchivovat", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "Nahrát", "create_album_page_untitled": "Bez názvu", "create_shared_album_page_create": "Vytvořit", "create_shared_album_page_share": "Sdílet", @@ -148,8 +154,8 @@ "delete_dialog_cancel": "Zrušit", "delete_dialog_ok": "Vymazat", "delete_dialog_title": "Vymazat trvale", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_content": "Opravdu chcete tento odkaz ke sdílení odstranit?", + "delete_shared_link_dialog_title": "Odstranit sdílený odkaz", "description_input_hint_text": "Přidat popis...", "description_input_submit_error": "Chyba aktualizace popisu, další podrobnosti najdete v logu", "exif_bottom_sheet_description": "Přidat popis...", @@ -164,14 +170,19 @@ "home_page_add_to_album_conflicts": "Přidáno {added} položek do alba {album}. {failed} položek již je v albu.", "home_page_add_to_album_err_local": "Zatím není možné přidat lokální média do alb, přeskakuji", "home_page_add_to_album_success": "Přidány položky {added} do alba {album}.", + "home_page_album_err_partner": "Položky partnera nelze zatím přidat do alba, přeskakuji", "home_page_archive_err_local": "Zatím nemohu archivovat lokální média, přeskakuji", + "home_page_archive_err_partner": "Položky partnera nelze archivovat, přeskakuji", "home_page_building_timeline": "Vytváření časové osy", + "home_page_delete_err_partner": "Položky partnera nelze odstranit, přeskakuji", "home_page_favorite_err_local": "Zatím není možné zařadit lokální média mezi oblíbená, přeskakuji", + "home_page_favorite_err_partner": "Položky partnera nelze označit jako oblíbené, přeskakuji", "home_page_first_time_notice": "Pokud aplikaci používáte poprvé, nezapomeňte si vybrat zálohovaná alba, aby se na časové ose mohly nacházet fotografie a videa z vybraných alb.", - "home_page_upload_err_limit": "Lze zálohovat nejvýše 30 položek najednou, přeskakuji", + "home_page_share_err_local": "Can not share local assets via link, skipping", + "home_page_upload_err_limit": "Lze nahrát nejvýše 30 položek najednou, přeskakuji", "image_viewer_page_state_provider_download_error": "Chyba stahování", "image_viewer_page_state_provider_download_success": "Stahování bylo úspěšné", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "Chyba sdílení", "library_page_albums": "Alba", "library_page_archive": "Archív", "library_page_device_albums": "Alba v zařízení", @@ -179,8 +190,8 @@ "library_page_new_album": "Nové album", "library_page_sharing": "Sdílení", "library_page_sort_created": "Naposledy vytvořené", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "Naposledy upraveno", + "library_page_sort_most_recent_photo": "Nejnovější fotografie", "library_page_sort_title": "Podle názvu alba", "login_disabled": "Přihlášení bylo zakázáno", "login_form_api_exception": "Výjimka API. Zkontrolujte URL serveru a zkuste to znovu.", @@ -218,7 +229,7 @@ "map_settings_dialog_cancel": "Zrušit", "map_settings_dialog_save": "Uložit", "map_settings_dialog_title": "Nastavení map", - "map_settings_include_show_archived": "Include Archived", + "map_settings_include_show_archived": "Zahrnout archivované", "map_settings_only_relative_range": "Rozsah data", "map_settings_only_show_favorites": "Zobrazit pouze oblíbené", "map_zoom_to_see_photos": "Oddálit pro zobrazení fotografií", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} již nebude mít přístup k vašim fotografiím.", "partner_page_stop_sharing_title": "Přestat sdílet vaše fotografie?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Přesto pokračovat", "permission_onboarding_get_started": "Začít", "permission_onboarding_go_to_settings": "Přejít do nastavení", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich potřebuje přístup k zobrazení vašich fotek a videí.", "profile_drawer_app_logs": "Logy", "profile_drawer_client_server_up_to_date": "Klient a server jsou aktuální", + "profile_drawer_documentation": "Dokumentace", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Nastavení", "profile_drawer_sign_out": "Odhlásit se", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "Vyhodit", "recently_added_page_title": "Nedávno přidané", "search_bar_hint": "Prohledejte své fotky", "search_page_categories": "Kategorie", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Nepodařilo se vytvořit album", "select_user_for_sharing_page_share_suggestions": "Návrhy", "server_info_box_app_version": "Verze aplikace", + "server_info_box_server_url": "URL serveru", "server_info_box_server_version": "Verze serveru", "setting_image_viewer_help": "V prohlížeči detailů se nejprve načte malá miniatura, poté se načte náhled střední velikosti (je-li povolen) a nakonec se načte originál (je-li povolen).", "setting_image_viewer_original_subtitle": "Umožňuje načíst původní obrázek v plném rozlišení (velký!). Zakázat pro snížení využití dat (v síti i v mezipaměti zařízení).", @@ -288,11 +303,11 @@ "setting_notifications_notify_minutes": "{} minut", "setting_notifications_notify_never": "nikdy", "setting_notifications_notify_seconds": "{} sekundy", - "setting_notifications_single_progress_subtitle": "Podrobné informace o průběhu zálohování položky", + "setting_notifications_single_progress_subtitle": "Podrobné informace o průběhu nahrávání položky", "setting_notifications_single_progress_title": "Zobrazit průběh detailů zálohování na pozadí", "setting_notifications_subtitle": "Přizpůsobení předvoleb oznámení", "setting_notifications_title": "Oznámení", - "setting_notifications_total_progress_subtitle": "Celkový průběh zálohování (hotovo/celkově)", + "setting_notifications_total_progress_subtitle": "Celkový průběh nahrání (hotovo/celkově)", "setting_notifications_total_progress_title": "Zobrazit celkový průběh zálohování na pozadí", "setting_pages_app_bar_settings": "Nastavení", "settings_require_restart": "Pro použití tohoto nastavení restartujte Immich", @@ -300,28 +315,37 @@ "share_add_photos": "Přidat fotografie", "share_add_title": "Přidat název", "share_create_album": "Vytvořit album", + "shared_album_activities_input_disable": "Komentář je vypnutý", + "shared_album_activities_input_hint": "Řekněte něco", + "shared_album_activity_remove_content": "Chcete odstranit tuto aktivitu?", + "shared_album_activity_remove_title": "Odstranit aktivitu", + "shared_album_activity_setting_subtitle": "Nechte ostatní reagovat", + "shared_album_activity_setting_title": "Komentáře a lajky", "share_dialog_preparing": "Připravuji...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_app_bar_title": "Sdílené odkazy", + "shared_link_create_app_bar_title": "Vytvořit odkaz pro sdílení", + "shared_link_create_info": "Umožnit každému, kdo má odkaz, zobrazit vybrané fotografie", + "shared_link_create_submit_button": "Vytvořit odkaz", + "shared_link_edit_allow_download": "Povolit veřejným uživatelům stahovat", + "shared_link_edit_allow_upload": "Povolit veřejným uživatelům nahrávat", + "shared_link_edit_app_bar_title": "Upravit odkaz", + "shared_link_edit_change_expiry": "Změnit dobu platnosti", + "shared_link_edit_description": "Popis", + "shared_link_edit_description_hint": "Zadejte popis sdílení", + "shared_link_edit_expire_after": "Platnost vyprší po", + "shared_link_edit_password": "Heslo", + "shared_link_edit_password_hint": "Zadejte heslo pro sdílení", + "shared_link_edit_show_meta": "Zobrazit metadata", + "shared_link_edit_submit_button": "Aktualizovat odkaz", + "shared_link_empty": "Nemáte žádné sdílené odkazy", + "shared_link_manage_links": "Spravovat sdílené odkazy", + "share_done": "Hotovo", "share_invite": "Pozvat do alba", "sharing_page_album": "Sdílená alba", "sharing_page_description": "Vytvářejte sdílená alba a sdílejte fotografie a videa s lidmi ve vaší síti.", "sharing_page_empty_list": "PRÁZDNÝ SEZNAM", "sharing_silver_appbar_create_shared_album": "Vytvořit sdílené album", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "Sdílené odkazy", "sharing_silver_appbar_share_partner": "Sdílet s partnerem", "tab_controller_nav_library": "Knihovna", "tab_controller_nav_photos": "Fotografie", @@ -338,29 +362,29 @@ "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í", "translated_text_options": "Možnosti", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", + "trash_page_delete": "Smazat", + "trash_page_delete_all": "Smazat všechny", + "trash_page_empty_trash_btn": "Vysypat koš", + "trash_page_empty_trash_dialog_content": "Chcete vyprázdnit svoje vyhozené položky? Tyto položky budou trvale odstraněny z aplikace", "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", + "trash_page_info": "Vyhozené položky budou trvale odstraněny po {} dnech", + "trash_page_no_assets": "Žádné vyhozené položky", + "trash_page_restore": "Obnovit", + "trash_page_restore_all": "Obnovit všechny", + "trash_page_select_assets_btn": "Vybrat položky", + "trash_page_select_btn": "Vybrat", + "trash_page_title": "Koš ({})", "upload_dialog_cancel": "Zrušit", "upload_dialog_info": "Chcete zálohovat vybrané položky na server?", - "upload_dialog_ok": "Zálohovat", - "upload_dialog_title": "Zálohovat položku", + "upload_dialog_ok": "Nahrát", + "upload_dialog_title": "Nahrát položku", "version_announcement_overlay_ack": "Potvrdit", "version_announcement_overlay_release_notes": "poznámky k vydání", "version_announcement_overlay_text_1": "Ahoj, k dispozici je nová verze", "version_announcement_overlay_text_2": "najděte si čas na návštěvu ", "version_announcement_overlay_text_3": " a ujistěte se, že vaše konfigurace docker-compose a .env je aktuální, abyste předešli nesprávné konfiguraci, zvláště pokud používáte WatchTower nebo jakýkoli mechanismus, který podporuje automatické aktualizace serverových aplikací.", "version_announcement_overlay_title": "K dispozici je nová verze serveru \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "Odstranit ze zásobníku", + "viewer_stack_use_as_main_asset": "Použít jako hlavní položku", + "viewer_unstack": "Rozbalit zásobník" } \ No newline at end of file diff --git a/mobile/assets/i18n/da-DK.json b/mobile/assets/i18n/da-DK.json index 41e2cc340..b5b7c7e45 100644 --- a/mobile/assets/i18n/da-DK.json +++ b/mobile/assets/i18n/da-DK.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Fejlede i at ændre albumtitel", "album_viewer_appbar_share_leave": "Forlad album", "album_viewer_appbar_share_remove": "Fjern fra album", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Tilføj brugere", "all_people_page_title": "Personer", "all_videos_page_title": "Videoer", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "Ingen arkiverede elementer blev fundet", "archive_page_title": "Arkivér ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Cacheforbrug", "cache_settings_subtitle": "Håndter cache-adfærden for Immich-appen.", "cache_settings_thumbnail_size": "Størrelse af miniaturebillede cache ({} elementer)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Cache-indstillinger", "change_password_form_confirm_password": "Bekræft kodeord", - "change_password_form_description": "Hej {firstName} {lastName},\n\nDette er enten første gang du logger ind eller også er der lavet en anmodning om at ændre dit kodeord. Indtast venligst et nyt kodeord nedenfor.", + "change_password_form_description": "Hej {name},\n\nDette er enten første gang du logger ind eller også er der lavet en anmodning om at ændre dit kodeord. Indtast venligst et nyt kodeord nedenfor.", "change_password_form_new_password": "Nyt kodeord", "change_password_form_password_mismatch": "Kodeord er ikke ens", "change_password_form_reenter_new_password": "Gentag nyt kodeord", @@ -130,8 +136,8 @@ "control_bottom_app_bar_delete": "Slet", "control_bottom_app_bar_favorite": "Favorit", "control_bottom_app_bar_share": "Del", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_share_to": "Del til", + "control_bottom_app_bar_stack": "Stak", "control_bottom_app_bar_unarchive": "Afakivér", "control_bottom_app_bar_upload": "Upload", "create_album_page_untitled": "Uden titel", @@ -148,8 +154,8 @@ "delete_dialog_cancel": "Annuller", "delete_dialog_ok": "Slet", "delete_dialog_title": "Slet permanent", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_content": "Er du sikker på, du vil slette dette delte link?", + "delete_shared_link_dialog_title": "Slet delt link", "description_input_hint_text": "Tilføj en beskrivelse...", "description_input_submit_error": "Fejl med at opdatere beskrivelsen. Tjek loggen for flere detaljer", "exif_bottom_sheet_description": "Tilføj beskrivelse...", @@ -164,14 +170,19 @@ "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": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Kan ikke arkivere lokalt element endnu.. Springer over", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Bygger tidslinjen", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Kan endnu ikke gøre lokale elementer til favoritter. Springer over..", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Hvis det er din første gang i appen, bedes du vælge en sikkerhedskopi af albummer så tidlinjen kan blive fyldt med billeder og videoer fra albummerne.", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Det er kun muligt at lave sikkerhedskopi af 30 elementer ad gangen. Springer over", "image_viewer_page_state_provider_download_error": "Fejl ved download", "image_viewer_page_state_provider_download_success": "Download succesfuld", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "Delingsfejl", "library_page_albums": "Albummer", "library_page_archive": "Arkiv", "library_page_device_albums": "Albummer på enhed", @@ -179,8 +190,8 @@ "library_page_new_album": "Nyt album", "library_page_sharing": "Delte", "library_page_sort_created": "Senest oprettet", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "Sidst redigeret", + "library_page_sort_most_recent_photo": "Seneste billede", "library_page_sort_title": "Albumtitel", "login_disabled": "Login er blevet deaktiveret", "login_form_api_exception": "API-undtagelse. Tjek serverens URL og prøv igen. ", @@ -218,7 +229,7 @@ "map_settings_dialog_cancel": "Annuller", "map_settings_dialog_save": "Gem", "map_settings_dialog_title": "Kortindstillinger", - "map_settings_include_show_archived": "Include Archived", + "map_settings_include_show_archived": "Inkluder arkiveret", "map_settings_only_relative_range": "Datointerval", "map_settings_only_show_favorites": "Vis kun favoritter", "map_zoom_to_see_photos": "Zoom ud for at vise billeder", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} vil ikke længere have adgang til dine billeder.", "partner_page_stop_sharing_title": "Stop med at dele dine billeder?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Fortsæt alligevel", "permission_onboarding_get_started": "Kom i gang", "permission_onboarding_go_to_settings": "Gå til indstillinger", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich kræver tilliadelse til at se dine billeder og videoer.", "profile_drawer_app_logs": "Log", "profile_drawer_client_server_up_to_date": "Klient og server er ajour", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Indstillinger", "profile_drawer_sign_out": "Log ud", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "Papirkurv", "recently_added_page_title": "Nyligt tilføjet", "search_bar_hint": "Søg i dine billeder", "search_page_categories": "Kategorier", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Fejlede i at oprette et nyt album", "select_user_for_sharing_page_share_suggestions": "Anbefalinger", "server_info_box_app_version": "Applikationsversion", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "Serverversion ", "setting_image_viewer_help": "Detaljeret visning indlæser miniaturebilleder først. Herefter indlæses mediumstørrelse forhåndsvisning af billedet (hvis dette er slået til), for til sidst at vise originalen (hvis dette er slået til).", "setting_image_viewer_original_subtitle": "Slå indlæsning af originalbillede i fuld størrelse til (stort!). Deaktiver for at reducere dataforbruget (både på netværket og for enhedscache).", @@ -300,28 +315,37 @@ "share_add_photos": "Tilføj billeder", "share_add_title": "Tilføj en titel", "share_create_album": "Opret album", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Forbereder...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_app_bar_title": "Delte links", + "shared_link_create_app_bar_title": "Opret link for at dele", + "shared_link_create_info": "Lad enhver med linket se de(t) valgte billede(r)", + "shared_link_create_submit_button": "Oprat link", + "shared_link_edit_allow_download": "Tillad at en offenlig bruger kan downloade", + "shared_link_edit_allow_upload": "Tillad at en offentlig bruger kan uploade", + "shared_link_edit_app_bar_title": "Rediger link", + "shared_link_edit_change_expiry": "Ændrer udløbstidspunkt", + "shared_link_edit_description": "Beskrivelse", + "shared_link_edit_description_hint": "Indtast beskrivelse", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", + "shared_link_edit_show_meta": "Vis metadata", + "shared_link_edit_submit_button": "Opdater link", + "shared_link_empty": "Du har endnu ingen delte links", + "shared_link_manage_links": "Håndter delte links", + "share_done": "Færdig", "share_invite": "Inviter til album", "sharing_page_album": "Delt albums", "sharing_page_description": "Opret delte albummer for at dele billeder og video med personer på dit netværk.", "sharing_page_empty_list": "TOM LISTE", "sharing_silver_appbar_create_shared_album": "Opret delt album", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "Delte links", "sharing_silver_appbar_share_partner": "Del med partner", "tab_controller_nav_library": "Bibliotek", "tab_controller_nav_photos": "Billeder", @@ -338,18 +362,18 @@ "theme_setting_three_stage_loading_subtitle": "Tre-trins indlæsning kan øge ydeevnen, men kan ligeledes føre til højere netværksbelastning", "theme_setting_three_stage_loading_title": "Slå tre-trins indlæsning til", "translated_text_options": "Handlinger", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", + "trash_page_delete": "Slet", + "trash_page_delete_all": "Slet alt", + "trash_page_empty_trash_btn": "Tøm papirkurv", + "trash_page_empty_trash_dialog_content": "Vil du tømme papirkurven? Disse elementer vil blive permanent fjernet fra Immich", "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", + "trash_page_info": "Slettede elementer vil blive slettet permanent efter {} dage", + "trash_page_no_assets": "Ingen slettede elementer", + "trash_page_restore": "Gendan", + "trash_page_restore_all": "Gendan alt", + "trash_page_select_assets_btn": "Vælg elementer", + "trash_page_select_btn": "Vælg", + "trash_page_title": "Papirkurv ({})", "upload_dialog_cancel": "Annuller", "upload_dialog_info": "Vil du sikkerhedskopiere de(t) valgte element(er) til serveren?", "upload_dialog_ok": "Upload", @@ -360,7 +384,7 @@ "version_announcement_overlay_text_2": ". Besøg venligst ", "version_announcement_overlay_text_3": " for at sikre dig, at din dockercompose- og .env-fil er opdateret, så der undgås fejlkonfiguration, specielt hvis du bruger WatchTower eller lignede.", "version_announcement_overlay_title": "Ny serverversion er tilgængelig \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "Fjern fra stak", + "viewer_stack_use_as_main_asset": "Brug som hovedelement", + "viewer_unstack": "Fjern fra stak" } \ No newline at end of file diff --git a/mobile/assets/i18n/de-DE.json b/mobile/assets/i18n/de-DE.json index 76085a61b..2af7aecc1 100644 --- a/mobile/assets/i18n/de-DE.json +++ b/mobile/assets/i18n/de-DE.json @@ -3,8 +3,8 @@ "add_to_album_bottom_sheet_already_exists": "Bereits in {album}", "advanced_settings_prefer_remote_subtitle": "Manche Endgeräte laden Vorschaubilder lokaler Bilder sehr langsam. Durch diese Einstellung werden diese stattdessen direkt vom Server geladen.", "advanced_settings_prefer_remote_title": "Server-Bilder bevorzugen", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_self_signed_ssl_subtitle": "Verifizierung von SSL-Zertifikaten vom Server überspringen. Notwendig bei selbstsignierten Zertifikaten.", + "advanced_settings_self_signed_ssl_title": "Selbstsignierte SSL-Zertifikate erlauben", "advanced_settings_tile_subtitle": "Erweiterte Benutzereinstellungen", "advanced_settings_tile_title": "Sonstige", "advanced_settings_troubleshooting_subtitle": "Aktiviere erweiterte Funktionen zur Fehlersuche", @@ -14,7 +14,7 @@ "album_thumbnail_card_item": "1 Element", "album_thumbnail_card_items": "{} Elemente", "album_thumbnail_card_shared": " · Geteilt", - "album_thumbnail_owned": "Owned", + "album_thumbnail_owned": "Eigene", "album_thumbnail_shared_by": "Geteilt von {}", "album_viewer_appbar_share_delete": "Album löschen", "album_viewer_appbar_share_err_delete": "Album konnte nicht gelöscht werden", @@ -22,12 +22,16 @@ "album_viewer_appbar_share_err_remove": "Beim Löschen von Elementen aus dem Album ist ein Problem aufgetreten", "album_viewer_appbar_share_err_title": "Der Titel konnte nicht geändert werden", "album_viewer_appbar_share_leave": "Album verlassen", - "album_viewer_appbar_share_remove": "Entferne vom Album", + "album_viewer_appbar_share_remove": "Vom Album entfernen", + "album_viewer_appbar_share_to": "Teile über", "album_viewer_page_share_add_users": "Nutzer hinzufügen", "all_people_page_title": "Personen", "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "Bist du sicher, dass du dich abmelden möchtest?", + "app_bar_signout_dialog_ok": "Ja", + "app_bar_signout_dialog_title": "Abmelden", "archive_page_no_archived_assets": "Keine archivierten Inhalte gefunden", - "archive_page_title": "Archive ({})", + "archive_page_title": "Archiv ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamisches Layout", "asset_list_layout_settings_group_automatically": "Automatisch", "asset_list_layout_settings_group_by": "Gruppiere Elemente nach", @@ -54,13 +58,13 @@ "backup_controller_page_background_app_refresh_disabled_title": "Hintergrundaktualisierungen sind deaktiviert.", "backup_controller_page_background_app_refresh_enable_button_text": "Gehe zu Einstellungen", "backup_controller_page_background_battery_info_link": "Zeige mir wie", - "backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.", + "backup_controller_page_background_battery_info_message": "Für die besten Ergebnisse für Sicherungen im Hintergrund, deaktiviere alle Batterieoptimierungen und Einschränkungen für die Hintergrundaktivitäten von Immich.\n\nDa dies gerätespezifisch ist, schlage diese Informationen für deinen Gerätehersteller nach.", "backup_controller_page_background_battery_info_ok": "OK", "backup_controller_page_background_battery_info_title": "Batterieoptimierungen", "backup_controller_page_background_charging": "Nur während des Ladens", "backup_controller_page_background_configure_error": "Konnte Hintergrundservice nicht konfigurieren", - "backup_controller_page_background_delay": "Delay new assets backup: {}", - "backup_controller_page_background_description": "Turn on the background service to automatically backup any new assets without needing to open the app", + "backup_controller_page_background_delay": "Sicherung neuer Elemente verzögern: {}", + "backup_controller_page_background_description": "Schalte den Hintergrundservice ein, um neue Elemente automatisch im Hintergrund zu sichern ohne die App zu öffnen", "backup_controller_page_background_is_off": "Automatische Sicherung im Hintergrund ist deaktiviert", "backup_controller_page_background_is_on": "Automatische Sicherung im Hintergrund ist aktiviert", "backup_controller_page_background_turn_off": "Hintergrundservice ausschalten", @@ -78,7 +82,7 @@ "backup_controller_page_id": "ID: {}", "backup_controller_page_info": "Informationen zur Sicherung", "backup_controller_page_none_selected": "Keine ausgewählt", - "backup_controller_page_remainder": "Übrig", + "backup_controller_page_remainder": "Verbleibend", "backup_controller_page_remainder_sub": "Noch zu sichernde Fotos und Videos", "backup_controller_page_select": "Auswählen", "backup_controller_page_server_storage": "Server Speicher", @@ -94,46 +98,48 @@ "backup_controller_page_uploading_file_info": "Informationen", "backup_err_only_album": "Das einzige Album kann nicht entfernt werden", "backup_info_card_assets": "Elemente", - "backup_manual_cancelled": "Cancelled", - "backup_manual_failed": "Failed", - "backup_manual_in_progress": "Upload already in progress. Try after sometime", - "backup_manual_success": "Success", - "backup_manual_title": "Upload status", - "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", + "backup_manual_cancelled": "Abgebrochen", + "backup_manual_failed": "Fehlgeschlagen", + "backup_manual_in_progress": "Sicherung läuft bereits. Bitte später erneut versuchen", + "backup_manual_success": "Erfolgreich", + "backup_manual_title": "Sicherungsstatus", + "cache_settings_album_thumbnails": "Vorschaubilder der Bibliothek ({} Elemente)", "cache_settings_clear_cache_button": "Zwischenspeicher löschen", "cache_settings_clear_cache_button_title": "Löscht den Zwischenspeicher der App. Dies wird die Leistungsfähigkeit der App deutlich einschränken, bis der Zwischenspeicher wieder aufgebaut wurde.", "cache_settings_image_cache_size": "{} Bilder im Zwischenspeicher", - "cache_settings_statistics_album": "Library thumbnails", - "cache_settings_statistics_assets": "{} assets ({})", - "cache_settings_statistics_full": "Full images", - "cache_settings_statistics_shared": "Shared album thumbnails", + "cache_settings_statistics_album": "Vorschaubilder der Bibliothek", + "cache_settings_statistics_assets": "{} Elemente ({})", + "cache_settings_statistics_full": "Originalbilder", + "cache_settings_statistics_shared": "Vorschaubilder geteilter Alben", "cache_settings_statistics_thumbnail": "Vorschaubilder", "cache_settings_statistics_title": "Zwischenspeicher Nutzung", "cache_settings_subtitle": "Kontrolliere wie Immich den Zwischenspeicher nutzen soll", "cache_settings_thumbnail_size": "{} Vorschaubilder im Zwischenspeicher", + "cache_settings_tile_subtitle": "Lokalen Speicher verwalten", + "cache_settings_tile_title": "Lokaler Speicher", "cache_settings_title": "Zwischenspeicher Einstellungen", "change_password_form_confirm_password": "Passwort bestätigen", - "change_password_form_description": "Hallo {firstName} {lastName}\n\nDas ist entweder das erste Mal dass du dich einloggst oder eine Anfrage zur Änderung deines Passwortes wurde gestellt. Bitte gebe das neue Passwort ein.", + "change_password_form_description": "Hallo {name}\n\nDas ist entweder das erste Mal dass du dich einloggst oder eine Anfrage zur Änderung deines Passwortes wurde gestellt. Bitte gebe das neue Passwort ein.", "change_password_form_new_password": "Neues Passwort", "change_password_form_password_mismatch": "Passwörter stimmen nicht überein", "change_password_form_reenter_new_password": "Passwort erneut eingeben", "common_add_to_album": "Zu Album hinzufügen", "common_change_password": "Passwort ändern", - "common_create_new_album": "Erstelle ein neues Album", + "common_create_new_album": "Neues Album erstellen", "common_server_error": "Bitte überprüfe Deine Netzwerkverbindung und stelle sicher, dass die App und Server Versionen kompatibel sind.", "common_shared": "Geteilt", "control_bottom_app_bar_add_to_album": "Zu Album hinzufügen", "control_bottom_app_bar_album_info": "{} Elemente", - "control_bottom_app_bar_album_info_shared": "{} Elemente · geteilt", + "control_bottom_app_bar_album_info_shared": "{} Elemente · Geteilt", "control_bottom_app_bar_archive": "Archiv", "control_bottom_app_bar_create_new_album": "Neues Album erstellen", "control_bottom_app_bar_delete": "Löschen", "control_bottom_app_bar_favorite": "Favorit", "control_bottom_app_bar_share": "Teilen", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_share_to": "Teilen mit", + "control_bottom_app_bar_stack": "Stapeln", "control_bottom_app_bar_unarchive": "Dearchivieren", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "Hochladen", "create_album_page_untitled": "Unbenannt", "create_shared_album_page_create": "Erstellen", "create_shared_album_page_share": "Teilen", @@ -147,16 +153,16 @@ "delete_dialog_alert": "Diese Elemente werden unwiderruflich von Immich und dem Gerät entfernt", "delete_dialog_cancel": "Abbrechen", "delete_dialog_ok": "Löschen", - "delete_dialog_title": "Für immer löschen", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_dialog_title": "Endgültig löschen", + "delete_shared_link_dialog_content": "Bist du sicher, dass du diesen geteilten Link löschen möchtest?", + "delete_shared_link_dialog_title": "Geteilten Link löschen", "description_input_hint_text": "Beschreibung hinzufügen...", "description_input_submit_error": "Beschreibung konnte nicht geändert werden, bitte im Log für mehr Details nachsehen.", "exif_bottom_sheet_description": "Beschreibung hinzufügen...", "exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_location": "STANDORT", "experimental_settings_new_asset_list_subtitle": "In Arbeit", - "experimental_settings_new_asset_list_title": "Experimentelle Fotogitter aktivieren", + "experimental_settings_new_asset_list_title": "Experimentelles Fotogitter aktivieren", "experimental_settings_subtitle": "Benutzung auf eigene Gefahr!", "experimental_settings_title": "Experimentell", "favorites_page_no_favorites": "Keine favorisierten Inhalte gefunden", @@ -164,39 +170,44 @@ "home_page_add_to_album_conflicts": "{added} Elemente zu {album} hinzugefügt. {failed} Elemente sind bereits vorhanden.", "home_page_add_to_album_err_local": "Kann lokale Elemente noch nicht zu Alben hinzufügen, überspringe", "home_page_add_to_album_success": "{added} Elemente zu {album} hinzugefügt.", + "home_page_album_err_partner": "Inhalte von Partnern können derzeit nicht zu Alben hinzugefügt werden", "home_page_archive_err_local": "Kann lokale Elemente nicht archvieren, überspringe", + "home_page_archive_err_partner": "Inhalte von Partnern können nicht archiviert werden", "home_page_building_timeline": "Zeitachse wird erstellt.", + "home_page_delete_err_partner": "Inhalte von Partnern können nicht gelöscht werden", "home_page_favorite_err_local": "Kann lokale Elemente noch nicht favorisieren, überspringe", + "home_page_favorite_err_partner": "Inhalte von Partnern können nicht favorisiert werden", "home_page_first_time_notice": "Wenn dies das erste Mal ist dass Du Immich nutzt, stelle bitte sicher, dass mindestens ein Album zur Sicherung ausgewählt ist, sodass die Zeitachse mit Fotos und Videos gefüllt werden kann.", - "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", + "home_page_share_err_local": "Can not share local assets via link, skipping", + "home_page_upload_err_limit": "Max. 30 Elemente können gleichzeitig hochgeladen werden, überspringe", "image_viewer_page_state_provider_download_error": "Fehler beim Herunterladen", "image_viewer_page_state_provider_download_success": "Erfolgreich heruntergeladen", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "Fehler beim Teilen", "library_page_albums": "Alben", "library_page_archive": "Archiv", - "library_page_device_albums": "Alben auf dem Gerät.", + "library_page_device_albums": "Alben auf dem Gerät", "library_page_favorites": "Favoriten", "library_page_new_album": "Neues Album", "library_page_sharing": "Teilen", "library_page_sort_created": "Zuletzt erstellt", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", - "library_page_sort_title": "Albumtitel", - "login_disabled": "Login has been disabled", + "library_page_sort_last_modified": "Zuletzt bearbeitet", + "library_page_sort_most_recent_photo": "Neuestes Foto", + "library_page_sort_title": "Titel des Albums", + "login_disabled": "Login ist deaktiviert", "login_form_api_exception": "API Fehler. Bitte die Serveradresse überprüfen und erneut versuchen.", "login_form_button_text": "Anmelden", "login_form_email_hint": "deine@email.de", "login_form_endpoint_hint": "http://deine-server-ip:port/api", - "login_form_endpoint_url": "Server URL", + "login_form_endpoint_url": "Server-URL", "login_form_err_http": "Bitte gebe http:// oder https:// an", "login_form_err_invalid_email": "Ungültige E-Mail", "login_form_err_invalid_url": "Ungültige URL", - "login_form_err_leading_whitespace": "Leerzichen am Anfang", + "login_form_err_leading_whitespace": "Leerzeichen am Anfang", "login_form_err_trailing_whitespace": "Leerzeichen am Ende", "login_form_failed_get_oauth_server_config": "Fehler beim Login per OAuth, Server-URL überprüfen", "login_form_failed_get_oauth_server_disable": "OAuth-Funktion nicht verfügbar auf diesem Server.", - "login_form_failed_login": "Error logging you in, check server url, email and password", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", + "login_form_failed_login": "Fehler beim Login, prüfe Server-URL, E-Mail und Passwort", + "login_form_handshake_exception": "Fehler beim Verbindungsaufbau mit dem Server. Falls du ein selbstsigniertes Zertifikat verwendest, aktiviere die Unterstützung in den Einstellungen.", "login_form_label_email": "E-Mail", "login_form_label_password": "Passwort", "login_form_next_button": "Weiter", @@ -204,26 +215,26 @@ "login_form_save_login": "Angemeldet bleiben", "login_form_server_empty": "Serveradresse eingeben.", "login_form_server_error": "Konnte nicht mit Server verbinden.", - "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", - "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_cancel": "Cancel", - "map_location_dialog_yes": "Yes", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", - "map_location_service_disabled_title": "Location Service disabled", - "map_no_assets_in_bounds": "No photos in this area", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", - "map_no_location_permission_title": "Location Permission denied", - "map_settings_dark_mode": "Dark mode", - "map_settings_dialog_cancel": "Cancel", - "map_settings_dialog_save": "Save", - "map_settings_dialog_title": "Map Settings", - "map_settings_include_show_archived": "Include Archived", - "map_settings_only_relative_range": "Date range", - "map_settings_only_show_favorites": "Show Favorite Only", - "map_zoom_to_see_photos": "Zoom out to see photos", + "login_password_changed_error": "Fehler beim Passwort ändern", + "login_password_changed_success": "Passwort erfolgreich geändert", + "map_cannot_get_user_location": "Standort konnte nicht ermittelt werden", + "map_location_dialog_cancel": "Abbrechen", + "map_location_dialog_yes": "Ja", + "map_location_service_disabled_content": "Ortungsdienste müssen aktiviert sein, um Inhalte am aktuellen Standort anzuzeigen. Willst du die Ortungsdienste aktivieren?", + "map_location_service_disabled_title": "Ortungsdienste deaktiviert", + "map_no_assets_in_bounds": "Keine Fotos in dieser Gegend", + "map_no_location_permission_content": "Ortungsdienste müssen aktiviert sein, um Inhalte am aktuellen Standort anzuzeigen. Willst du die Ortungsdienste aktivieren?", + "map_no_location_permission_title": "Kein Zugriff auf den Standort", + "map_settings_dark_mode": "Dunkler Modus", + "map_settings_dialog_cancel": "Abbrechen", + "map_settings_dialog_save": "Speichern", + "map_settings_dialog_title": "Karteneinstellungen", + "map_settings_include_show_archived": "Archivierte anzeigen", + "map_settings_only_relative_range": "Datumsbereich", + "map_settings_only_show_favorites": "Nur Favoriten anzeigen", + "map_zoom_to_see_photos": "Ansicht verkleinern um Fotos zu sehen", "monthly_title_text_date_format": "MMMM y", - "motion_photos_page_title": "Live Photos", + "motion_photos_page_title": "Live-Fotos", "notification_permission_dialog_cancel": "Abbrechen", "notification_permission_dialog_content": "Um Benachrichtigungen zu aktivieren, navigiere zu Einstellungen und klicke \"Erlauben\"", "notification_permission_dialog_settings": "Einstellungen", @@ -231,33 +242,36 @@ "notification_permission_list_tile_enable_button": "Aktiviere Benachrichtigungen", "notification_permission_list_tile_title": "Benachrichtigungs-Berechtigung", "partner_page_add_partner": "Partner hinzufügen", - "partner_page_empty_message": "Your photos are not yet shared with any partner.", - "partner_page_no_more_users": "No more users to add", - "partner_page_partner_add_failed": "Failed to add partner", + "partner_page_empty_message": "Deine Fotos sind noch nicht geteilt mit einem Partner", + "partner_page_no_more_users": "Keine weiteren Nutzer", + "partner_page_partner_add_failed": "Fehler beim Partner hinzufügen", "partner_page_select_partner": "Partner auswählen", "partner_page_shared_to_title": "Geteilt mit", "partner_page_stop_sharing_content": "{} wird nicht mehr auf deine Fotos zugreifen können.", - "partner_page_stop_sharing_title": "Stop sharing your photos?", + "partner_page_stop_sharing_title": "Deine Fotos nicht mehr teilen?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Trotzdem fortfahren", - "permission_onboarding_get_started": "Get started", + "permission_onboarding_get_started": "Jetzt starten", "permission_onboarding_go_to_settings": "Gehe zu Einstellungen", "permission_onboarding_grant_permission": "Berechtigung erteilen", "permission_onboarding_log_out": "Abmelden", - "permission_onboarding_permission_denied": "Berechtigungen verweigert. Um Immich zu benutzen, Zugriff auf Fotos und Videos in Einstellungen erlauben.", + "permission_onboarding_permission_denied": "Berechtigung verweigert. Um Immich zu benutzen, muss Zugriff auf Fotos und Videos in Einstellungen erlaubt werden.", "permission_onboarding_permission_granted": "Berechtigung erteilt! Du bist startklar.", "permission_onboarding_permission_limited": "Berechtigungen unzureichend. Um Immich das Sichern von ganzen Sammlungen zu ermöglichen, muss der Zugriff auf alle Fotos und Videos in den Einstellungen erlaubt werden.", "permission_onboarding_request": "Immich benötigt Berechtigung um auf deine Fotos und Videos zuzugreifen.", "profile_drawer_app_logs": "Logs", "profile_drawer_client_server_up_to_date": "App und Server sind aktuell", + "profile_drawer_documentation": "Dokumentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Einstellungen", "profile_drawer_sign_out": "Abmelden", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "Papierkorb", "recently_added_page_title": "Zuletzt hinzugefügt", "search_bar_hint": "Durchsuche deine Fotos", "search_page_categories": "Kategorien", "search_page_favorites": "Favoriten", - "search_page_motion_photos": "Live Photos", + "search_page_motion_photos": "Live-Fotos", "search_page_no_objects": "Keine Objektinformationen verfügbar", "search_page_no_places": "Keine Informationen über Orte verfügbar", "search_page_people": "Personen", @@ -265,7 +279,7 @@ "search_page_recently_added": "Zuletzt hinzugefügt", "search_page_screenshots": "Bildschirmfotos", "search_page_selfies": "Selfies", - "search_page_things": "Dinge", + "search_page_things": "Gegenstände und Tiere", "search_page_videos": "Videos", "search_page_view_all_button": "Alle anzeigen", "search_page_your_activity": "Deine Aktivität", @@ -274,55 +288,65 @@ "search_suggestion_list_smart_search_hint_2": "m:dein-suchbegriff", "select_additional_user_for_sharing_page_suggestions": "Vorschläge", "select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden", - "select_user_for_sharing_page_share_suggestions": "Suggestions", + "select_user_for_sharing_page_share_suggestions": "Empfehlungen", "server_info_box_app_version": "App Version", + "server_info_box_server_url": "Server-URL", "server_info_box_server_version": "Server Version", "setting_image_viewer_help": "Der Detailbildbetrachter lädt zuerst die kleine Miniaturansicht, dann die Vorschau in mittlerer Größe (falls aktiviert) und schließlich das Original (falls aktiviert).", "setting_image_viewer_original_subtitle": "Aktivieren, um das Originalbild in voller Auflösung (groß!) zu laden. Deaktivieren, um den Datenverbrauch zu reduzieren (sowohl im Netzwerk als auch im Gerätespeicher).", "setting_image_viewer_original_title": "Original laden", "setting_image_viewer_preview_subtitle": "Aktivieren, um ein Bild mit mittlerer Auflösung zu laden. Deaktivieren, um entweder das Original direkt zu laden oder nur die Miniaturansicht zu verwenden.", "setting_image_viewer_preview_title": "Vorschaubild laden", - "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", + "setting_notifications_notify_failures_grace_period": "Benachrichtigung über Fehler bei der Hintergrundsicherung: {}", "setting_notifications_notify_hours": "{} Stunden", "setting_notifications_notify_immediately": "sofort", "setting_notifications_notify_minutes": "{} Minuten", "setting_notifications_notify_never": "niemals", "setting_notifications_notify_seconds": "{} Sekunden", - "setting_notifications_single_progress_subtitle": "Detaillierte Upload Informationen für jedes Element.", - "setting_notifications_single_progress_title": "Zeige Hintergrund-Sicherungs Detailfortschritt", - "setting_notifications_subtitle": "Passe Deine Benachrichtigungen an", + "setting_notifications_single_progress_subtitle": "Detaillierter Upload-Fortschritt für jedes Element.", + "setting_notifications_single_progress_title": "Zeige detaillierten Fortschritt bei der Hintergrundsicherung", + "setting_notifications_subtitle": "Benachrichtigungen anpassen", "setting_notifications_title": "Benachrichtigungen", "setting_notifications_total_progress_subtitle": "Gesamter Upload-Fortschritt (abgeschlossen/Anzahl Elemente)", - "setting_notifications_total_progress_title": "Zeige Hintergrundsicherungsfortschritt", + "setting_notifications_total_progress_title": "Zeige Gesamtfortschritt bei der Hintergrundsicherung", "setting_pages_app_bar_settings": "Einstellungen", "settings_require_restart": "Bitte starte Immich neu, um diese Einstellung anzuwenden.", "share_add": "Hinzufügen", "share_add_photos": "Fotos hinzufügen", "share_add_title": "Titel hinzufügen", "share_create_album": "Album erstellen", + "shared_album_activities_input_disable": "Kommentare sind deaktiviert.", + "shared_album_activities_input_hint": "Sag etwas", + "shared_album_activity_remove_content": "Möchtest du diese Aktivität entfernen?", + "shared_album_activity_remove_title": "Aktivität entfernen", + "shared_album_activity_setting_subtitle": "Lass andere reagieren.", + "shared_album_activity_setting_title": "Kommentare & Likes", "share_dialog_preparing": "Vorbereiten...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_app_bar_title": "Geteilte Links", + "shared_link_create_app_bar_title": "Link zum Teilen erstellen", + "shared_link_create_info": "Alle, die über den Link verfügen, können die Fotos sehen", + "shared_link_create_submit_button": "Link erstellen", + "shared_link_edit_allow_download": "Jeder darf herunterladen", + "shared_link_edit_allow_upload": "Jeder darf hochladen", + "shared_link_edit_app_bar_title": "Link bearbeiten", + "shared_link_edit_change_expiry": "Ablaufdatum bearbeiten", + "shared_link_edit_description": "Beschreibung", + "shared_link_edit_description_hint": "Beschreibung eingeben", + "shared_link_edit_expire_after": "Erlischt nach", + "shared_link_edit_password": "Passwort", + "shared_link_edit_password_hint": "Passwort eingeben", + "shared_link_edit_show_meta": "Metadaten anzeigen", + "shared_link_edit_submit_button": "Link aktualisieren", + "shared_link_empty": "Du hast keine geteilten Links", + "shared_link_manage_links": "Geteilte Links verwalten", + "share_done": "Fertig", "share_invite": "Zum Album einladen", "sharing_page_album": "Geteilte Alben", "sharing_page_description": "Erstelle ein geteiltes Album um Fotos und Videos mit Personen in deinem Netzwerk zu teilen.", "sharing_page_empty_list": "LEERE LISTE", "sharing_silver_appbar_create_shared_album": "Neues geteiltes Album", - "sharing_silver_appbar_shared_links": "Shared links", - "sharing_silver_appbar_share_partner": "Teile mit Partner", + "sharing_silver_appbar_shared_links": "Geteilte Links", + "sharing_silver_appbar_share_partner": "Mit Partner teilen", "tab_controller_nav_library": "Bibliothek", "tab_controller_nav_photos": "Fotos", "tab_controller_nav_search": "Suche", @@ -337,30 +361,30 @@ "theme_setting_theme_title": "Theme", "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", - "translated_text_options": "Options", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", + "translated_text_options": "Optionen", + "trash_page_delete": "Löschen", + "trash_page_delete_all": "Alle löschen", + "trash_page_empty_trash_btn": "Papierkorb leeren", + "trash_page_empty_trash_dialog_content": "Elemente im Papierkorb löschen? Diese Elemente werden dauerhaft von Immich entfernt", "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", - "upload_dialog_cancel": "Cancel", - "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", - "upload_dialog_ok": "Upload", - "upload_dialog_title": "Upload Asset", + "trash_page_info": "Elemente im Papierkorb werden nach {} Tagen endgültig gelöscht", + "trash_page_no_assets": "Keine Elemente im Papierkorb", + "trash_page_restore": "Wiederherstellen", + "trash_page_restore_all": "Alle wiederherstellen", + "trash_page_select_assets_btn": "Elemente auswählen", + "trash_page_select_btn": "Auswählen", + "trash_page_title": "Papierkorb ({})", + "upload_dialog_cancel": "Abbrechen", + "upload_dialog_info": "Willst du die ausgewählten Elemente auf dem Server sichern?", + "upload_dialog_ok": "Hochladen", + "upload_dialog_title": "Element hochladen", "version_announcement_overlay_ack": "Ich habe verstanden", "version_announcement_overlay_release_notes": "Änderungsprotokoll", "version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von", - "version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ", + "version_announcement_overlay_text_2": "Bitte nehme dir die Zeit und lies das ", "version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).", "version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "Aus Stapel entfernen", + "viewer_stack_use_as_main_asset": "An Stapelanfang", + "viewer_unstack": "Stapel aufheben" } \ No newline at end of file diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index f710bec06..6d28890eb 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -1,6 +1,7 @@ { "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", + "advanced_settings_log_level_title": "Log level: {}", "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", "advanced_settings_prefer_remote_title": "Prefer remote images", "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", @@ -27,6 +28,9 @@ "album_viewer_page_share_add_users": "Add users", "all_people_page_title": "People", "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", "archive_page_title": "Archive ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", @@ -103,6 +107,9 @@ "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", "cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", + "cache_settings_duplicated_assets_clear_button": "CLEAR", + "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", + "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", "cache_settings_image_cache_size": "Image cache size ({} assets)", "cache_settings_statistics_album": "Library thumbnails", "cache_settings_statistics_assets": "{} assets ({})", @@ -112,9 +119,11 @@ "cache_settings_statistics_title": "Cache usage", "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", @@ -165,10 +174,15 @@ "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", "home_page_add_to_album_success": "Added {added} assets to album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Can not archive local assets yet, skipping", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Building the timeline", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "image_viewer_page_state_provider_download_error": "Download Error", "image_viewer_page_state_provider_download_success": "Download Success", @@ -185,6 +199,7 @@ "library_page_sort_title": "Album title", "login_disabled": "Login has been disabled", "login_form_api_exception": "API exception. Please check the server URL and try again.", + "login_form_back_button_text": "Back", "login_form_button_text": "Login", "login_form_email_hint": "youremail@email.com", "login_form_endpoint_hint": "http://your-server-ip:port/api", @@ -207,6 +222,10 @@ "login_form_server_error": "Could not connect to server.", "login_password_changed_error": "There was an error updating your password", "login_password_changed_success": "Password updated successfully", + "map_assets_in_bounds": { + "one": "{} photo", + "other": "{} photos" + }, "map_cannot_get_user_location": "Cannot get user's location", "map_location_dialog_cancel": "Cancel", "map_location_dialog_yes": "Yes", @@ -216,6 +235,15 @@ "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", "map_no_location_permission_title": "Location Permission denied", "map_settings_dark_mode": "Dark mode", + "map_settings_date_range_option_all": "All", + "map_settings_date_range_option_days": { + "one": "Past 24 hours", + "other": "Past {} days" + }, + "map_settings_date_range_option_years": { + "one": "Past year", + "other": "Past {} years" + }, "map_settings_dialog_cancel": "Cancel", "map_settings_dialog_save": "Save", "map_settings_dialog_title": "Map Settings", @@ -240,6 +268,7 @@ "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Get started", "permission_onboarding_go_to_settings": "Go to settings", @@ -250,12 +279,16 @@ "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "profile_drawer_app_logs": "Logs", + "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", + "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", + "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", + "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", "profile_drawer_settings": "Settings", "profile_drawer_sign_out": "Sign Out", "profile_drawer_trash": "Trash", - "profile_drawer_documentation": "Documentation", - "profile_drawer_github": "GitHub", "recently_added_page_title": "Recently Added", "search_bar_hint": "Search your photos", "search_page_categories": "Categories", @@ -264,6 +297,13 @@ "search_page_no_objects": "No Objects Info Available", "search_page_no_places": "No Places Info Available", "search_page_people": "People", + "search_page_person_add_name_dialog_cancel": "Cancel", + "search_page_person_add_name_dialog_save": "Save", + "search_page_person_add_name_dialog_hint": "Name", + "search_page_person_add_name_dialog_title": "Add a name", + "search_page_person_add_name_subtitle": "Find them fast by name with search", + "search_page_person_add_name_title": "Add a name", + "search_page_person_edit_name": "Edit name", "search_page_places": "Places", "search_page_recently_added": "Recently added", "search_page_screenshots": "Screenshots", @@ -272,6 +312,7 @@ "search_page_videos": "Videos", "search_page_view_all_button": "View all", "search_page_your_activity": "Your activity", + "search_page_your_map": "Your Map", "search_result_page_new_search_hint": "New Search", "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ", "search_suggestion_list_smart_search_hint_2": "m:your-search-term", @@ -279,8 +320,9 @@ "select_user_for_sharing_page_err_album": "Failed to create album", "select_user_for_sharing_page_share_suggestions": "Suggestions", "server_info_box_app_version": "App Version", - "server_info_box_server_version": "Server Version", "server_info_box_server_url": "Server URL", + "server_info_box_server_version": "Server Version", + "server_info_box_latest_release":"Latest Version", "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", "setting_image_viewer_original_title": "Load original image", @@ -304,9 +346,23 @@ "share_add_photos": "Add photos", "share_add_title": "Add a title", "share_create_album": "Create album", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", + "shared_album_section_people_action_error": "Error leaving/removing from album", + "shared_album_section_people_action_leave": "Remove user from album", + "shared_album_section_people_action_remove_user": "Remove user from album", + "shared_album_section_people_owner_label": "Owner", + "shared_album_section_people_title": "PEOPLE", "share_dialog_preparing": "Preparing...", "shared_link_app_bar_title": "Shared Links", + "shared_link_clipboard_copied_massage": "Copied to clipboard", + "shared_link_clipboard_text": "Link: {}\nPassword: {}", "shared_link_create_app_bar_title": "Create link to share", + "shared_link_create_error": "Error while creating shared link", "shared_link_create_info": "Let anyone with the link see the selected photo(s)", "shared_link_create_submit_button": "Create link", "shared_link_edit_allow_download": "Allow public user to download", @@ -315,18 +371,54 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_expire_after_option_days": { + "one": "{} day", + "other": "{} days" + }, + "shared_link_edit_expire_after_option_hours": { + "one": "{} hour", + "other": "{} hours" + }, + "shared_link_edit_expire_after_option_minutes": { + "one": "{} minute", + "other": "{} minutes" + }, + "shared_link_edit_expire_after_option_never": "Never", "shared_link_edit_password": "Password", "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", + "shared_link_error_server_url_fetch": "Cannot fetch the server url", + "shared_link_expired": "Expired", + "shared_link_expires_days": { + "one": "Expires in {} day", + "other": "Expires in {} days" + }, + "shared_link_expires_hours": { + "one": "Expires in {} hour", + "other": "Expires in {} hours" + }, + "shared_link_expires_minutes": { + "one": "Expires in {} minute", + "other": "Expires in {} minutes" + }, + "shared_link_expires_seconds": { + "one": "Expires in {} second", + "other": "Expires in {} seconds" + }, + "shared_link_expires_never": "Expires ∞", + "shared_link_info_chip_download": "Download", + "shared_link_info_chip_metadata": "EXIF", + "shared_link_info_chip_upload": "Upload", "shared_link_manage_links": "Manage Shared links", "share_done": "Done", "share_invite": "Invite to album", "sharing_page_album": "Shared albums", "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", "sharing_page_empty_list": "EMPTY LIST", - "sharing_silver_appbar_create_shared_album": "Create shared album", + "sharing_silver_appbar_create_shared_album": "New shared album", "sharing_silver_appbar_shared_links": "Shared links", "sharing_silver_appbar_share_partner": "Share with partner", "tab_controller_nav_library": "Library", @@ -367,14 +459,7 @@ "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", "viewer_remove_from_stack": "Remove from Stack", - "viewer_unstack": "Un-Stack", - "cache_settings_tile_title": "Local Storage", - "cache_settings_tile_subtitle": "Control the local storage behaviour", "viewer_stack_use_as_main_asset": "Use as Main Asset", - "app_bar_signout_dialog_title": "Sign out", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", - "app_bar_signout_dialog_ok": "Yes", - "shared_album_activities_input_hint": "Say something", - "shared_album_activity_remove_title": "Delete Activity", - "shared_album_activity_remove_content": "Do you want to delete this activity?" + "viewer_unstack": "Un-Stack", + "scaffold_body_error_occured": "Error occured" } diff --git a/mobile/assets/i18n/es-ES.json b/mobile/assets/i18n/es-ES.json index 7e68fe759..f32a8b1f8 100644 --- a/mobile/assets/i18n/es-ES.json +++ b/mobile/assets/i18n/es-ES.json @@ -3,8 +3,8 @@ "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}", "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de recursos encontrados el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.", "advanced_settings_prefer_remote_title": "Preferir imágenes remotas", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados", + "advanced_settings_self_signed_ssl_title": "Permitir certificados autofirmados", "advanced_settings_tile_subtitle": "Configuraciones avanzadas del usuario", "advanced_settings_tile_title": "Avanzado", "advanced_settings_troubleshooting_subtitle": "Habilitar funciones adicionales para solución de problemas", @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Error al cambiar el título del álbum ", "album_viewer_appbar_share_leave": "Abandonar álbum ", "album_viewer_appbar_share_remove": "Eliminar del álbum ", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Agregar usuarios", "all_people_page_title": "Personas", "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "¿Estás seguro que quieres cerrar sesión?", + "app_bar_signout_dialog_ok": "Sí", + "app_bar_signout_dialog_title": "Cerrar sesión", "archive_page_no_archived_assets": "No se encontraron recursos archivados", "archive_page_title": "Archivo ({})", "asset_list_layout_settings_dynamic_layout_title": "Diseño dinámico", @@ -59,7 +63,7 @@ "backup_controller_page_background_battery_info_title": "Optimizaciones de batería", "backup_controller_page_background_charging": "Solo mientras se carga", "backup_controller_page_background_configure_error": "Error al configurar el servicio en segundo plano", - "backup_controller_page_background_delay": "Retraso en la copia de seguridad de nuevos activos: {}", + "backup_controller_page_background_delay": "Retraso en la copia de seguridad de nuevos elementos: {}", "backup_controller_page_background_description": "Activa el servicio en segundo plano para copiar automáticamente cualquier nuevos archivos sin necesidad de abrir la aplicación.", "backup_controller_page_background_is_off": "La copia de seguridad en segundo plano automática está desactivada", "backup_controller_page_background_is_on": "La copia de seguridad en segundo plano automática está activada", @@ -71,7 +75,7 @@ "backup_controller_page_backup_sub": "Fotos y videos respaldados", "backup_controller_page_cancel": "Cancelar", "backup_controller_page_created": "Creado el: {}", - "backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos activos al servidor.", + "backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos elementos al servidor.", "backup_controller_page_excluded": "Excluido:", "backup_controller_page_failed": "Fallidos ({})", "backup_controller_page_filename": "Nombre del archivo: {} [{}]", @@ -94,11 +98,11 @@ "backup_controller_page_uploading_file_info": "Cargando información del archivo", "backup_err_only_album": "No se puede eliminar el único álbum", "backup_info_card_assets": "archivos", - "backup_manual_cancelled": "Cancelled", - "backup_manual_failed": "Failed", - "backup_manual_in_progress": "Upload already in progress. Try after sometime", - "backup_manual_success": "Success", - "backup_manual_title": "Upload status", + "backup_manual_cancelled": "Cancelado", + "backup_manual_failed": "Fallido", + "backup_manual_in_progress": "Subida en progreso. Espere", + "backup_manual_success": "Éxito", + "backup_manual_title": "Estado de la subida", "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} archivos)", "cache_settings_clear_cache_button": "Borrar caché", "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Uso de caché", "cache_settings_subtitle": "Controla el comportamiento del almacenamiento en caché de la aplicación móvil Immich", "cache_settings_thumbnail_size": "Tamaño de la caché de miniaturas ({} archivos)", + "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local", + "cache_settings_tile_title": "Almacenamiento local", "cache_settings_title": "Configuración de la caché", "change_password_form_confirm_password": "Confirmar Contraseña", - "change_password_form_description": "Hola {firstName} {lastName},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", + "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", "change_password_form_new_password": "Nueva Contraseña", "change_password_form_password_mismatch": "Las contraseñas no coinciden", "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña", @@ -130,10 +136,10 @@ "control_bottom_app_bar_delete": "Eliminar", "control_bottom_app_bar_favorite": "Favorito", "control_bottom_app_bar_share": "Compartir", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_share_to": "Enviar", + "control_bottom_app_bar_stack": "Apilar", "control_bottom_app_bar_unarchive": "Desarchivar", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "Subir", "create_album_page_untitled": "Sin título", "create_shared_album_page_create": "Crear", "create_shared_album_page_share": "Compartir", @@ -148,8 +154,8 @@ "delete_dialog_cancel": "Cancelar", "delete_dialog_ok": "Eliminar", "delete_dialog_title": "Eliminar Permanentemente", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_content": "Estás seguro que quieres eliminar este enlace compartido", + "delete_shared_link_dialog_title": "Eliminar enlace compartido", "description_input_hint_text": "Agregar descripción...", "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles", "exif_bottom_sheet_description": "Agregar Descripción...", @@ -164,14 +170,19 @@ "home_page_add_to_album_conflicts": "{added} elementos agregados al álbum {album}.{failed} elementos ya existen en el álbum.", "home_page_add_to_album_err_local": "Aún no se pueden agregar recursos locales a álbumes, omitiendo", "home_page_add_to_album_success": "{added} elementos agregados al álbum {album}. ", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Los recursos locales no pueden ser archivados, omitiendo", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Construyendo la línea de tiempo", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Aún no se pueden archivar recursos locales, omitiendo", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.", - "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", + "home_page_share_err_local": "Can not share local assets via link, skipping", + "home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo", "image_viewer_page_state_provider_download_error": "Error de descarga", "image_viewer_page_state_provider_download_success": "Descarga exitosa", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "Error al compartir", "library_page_albums": "Álbumes", "library_page_archive": "Archivo", "library_page_device_albums": "Álbumes en el dispositivo", @@ -179,10 +190,10 @@ "library_page_new_album": "Nuevo álbum", "library_page_sharing": "Compartiendo", "library_page_sort_created": "Creado más recientemente", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "Última modificación", + "library_page_sort_most_recent_photo": "Foto más reciente", "library_page_sort_title": "Título del álbum", - "login_disabled": "Login has been disabled", + "login_disabled": "El inicio de sesión ha sido desactivado", "login_form_api_exception": "Excepción producida por API. Por favor, verifica el URL del servidor e inténtalo de nuevo.", "login_form_button_text": "Iniciar Sesión", "login_form_email_hint": "tucorreo@correo.com", @@ -196,7 +207,7 @@ "login_form_failed_get_oauth_server_config": "Error al iniciar sesión con OAuth, verifica la URL del servidor", "login_form_failed_get_oauth_server_disable": "La función de OAuth no está disponible en este servidor", "login_form_failed_login": "Error al iniciar sesión, comprueba la URL del servidor, el correo electrónico y la contraseña", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", + "login_form_handshake_exception": "Hubo un error de verificación del certificado del servidor. Activa el soporte para certificados autofirmados en las preferencias si estás usando un certificado autofirmado", "login_form_label_email": "Correo", "login_form_label_password": "Contraseña", "login_form_next_button": "Siguiente", @@ -204,24 +215,24 @@ "login_form_save_login": "Mantener la sesión iniciada", "login_form_server_empty": "Agrega la URL del servidor.", "login_form_server_error": "No se pudo conectar al servidor.", - "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", - "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_cancel": "Cancel", - "map_location_dialog_yes": "Yes", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", - "map_location_service_disabled_title": "Location Service disabled", - "map_no_assets_in_bounds": "No photos in this area", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", - "map_no_location_permission_title": "Location Permission denied", - "map_settings_dark_mode": "Dark mode", - "map_settings_dialog_cancel": "Cancel", - "map_settings_dialog_save": "Save", - "map_settings_dialog_title": "Map Settings", - "map_settings_include_show_archived": "Include Archived", - "map_settings_only_relative_range": "Date range", - "map_settings_only_show_favorites": "Show Favorite Only", - "map_zoom_to_see_photos": "Zoom out to see photos", + "login_password_changed_error": "Hubo un error actualizando la contraseña", + "login_password_changed_success": "Contraseña cambiado con éxito", + "map_cannot_get_user_location": "No se pudo obtener la posición del usuario", + "map_location_dialog_cancel": "Cancelar", + "map_location_dialog_yes": "Sí", + "map_location_service_disabled_content": "Los servicios de ubicación deben estar activados para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?", + "map_location_service_disabled_title": "Servicios de ubicación desactivados", + "map_no_assets_in_bounds": "No hay fotos en esta zona", + "map_no_location_permission_content": "Se necesitan permisos de ubicación para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?", + "map_no_location_permission_title": "Permisos de ubicación denegados", + "map_settings_dark_mode": "Modo oscuro", + "map_settings_dialog_cancel": "Cancelar", + "map_settings_dialog_save": "Guardar", + "map_settings_dialog_title": "Ajustes mapa", + "map_settings_include_show_archived": "Incluir archivados", + "map_settings_only_relative_range": "Rango de fechas", + "map_settings_only_show_favorites": "Mostrar solo favoritas", + "map_zoom_to_see_photos": "Alejar para ver fotos", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Foto en Movimiento", "notification_permission_dialog_cancel": "Cancelar", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} ya no podrá acceder a tus fotos", "partner_page_stop_sharing_title": "¿Dejar de compartir tus fotos?", "partner_page_title": "Compañero", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continuar de todos modos", "permission_onboarding_get_started": "Empezar", "permission_onboarding_go_to_settings": "Ir a configuración", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.", "profile_drawer_app_logs": "Registros", "profile_drawer_client_server_up_to_date": "El Cliente y el Servidor están actualizados", + "profile_drawer_documentation": "Documentación", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Configuración", "profile_drawer_sign_out": "Cerrar Sesión", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "Papelera", "recently_added_page_title": "Recién Agregadas", "search_bar_hint": "Busca tus fotos", "search_page_categories": "Categorías", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Fallo al crear el álbum", "select_user_for_sharing_page_share_suggestions": "Sugerencias", "server_info_box_app_version": "Versión de la Aplicación", + "server_info_box_server_url": "URL del servidor", "server_info_box_server_version": "Versión del Servidor", "setting_image_viewer_help": "El visor de detalles carga primero la miniatura pequeña, luego carga la vista previa de tamaño mediano (si está habilitada), finalmente carga la original (si está habilitada).", "setting_image_viewer_original_subtitle": "Activar para cargar la imagen en resolución original (¡muy grande!). Deshabilitar para reducir el consumo de datos (de red y caché).", @@ -300,35 +315,44 @@ "share_add_photos": "Agregar fotos", "share_add_title": "Agregar un título", "share_create_album": "Crear álbum", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Preparando...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_app_bar_title": "Enlaces compartidos", + "shared_link_create_app_bar_title": "Crear enlace compartido", + "shared_link_create_info": "Cualquier persona con el enlace puede ver las fotos seleccionadas", + "shared_link_create_submit_button": "Crear enlace", + "shared_link_edit_allow_download": "Permitir descargar a usuarios públicos", + "shared_link_edit_allow_upload": "Permitir subir a usuarios públicos", + "shared_link_edit_app_bar_title": "Editar enlace", + "shared_link_edit_change_expiry": "Cambiar fecha de caducidad", + "shared_link_edit_description": "Descripción", + "shared_link_edit_description_hint": "Introduce la descripción del enlace", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Contraseña", + "shared_link_edit_password_hint": "Introduce la contraseña del enlace", + "shared_link_edit_show_meta": "Mostrar metadatos", + "shared_link_edit_submit_button": "Actualizar enlace", + "shared_link_empty": "No tienes enlaces compartidos", + "shared_link_manage_links": "Administrar enlaces compartidos", + "share_done": "Hecho", "share_invite": "Invitar al álbum", "sharing_page_album": "Álbumes compartidos", "sharing_page_description": "Crea álbumes compartidos para compartir fotos y vídeos con las personas de tu red.", "sharing_page_empty_list": "LISTA VACIA", "sharing_silver_appbar_create_shared_album": "Crear un álbum compartido", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "Enlaces compartidos", "sharing_silver_appbar_share_partner": "Compartir con el compañero", "tab_controller_nav_library": "Biblioteca", "tab_controller_nav_photos": "Fotos", "tab_controller_nav_search": "Buscar", "tab_controller_nav_sharing": "Compartiendo", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los archivos", - "theme_setting_asset_list_tiles_per_row_title": "Número de activos por fila ({})", + "theme_setting_asset_list_tiles_per_row_title": "Número de elementos por fila ({})", "theme_setting_dark_mode_switch": "Modo oscuro", "theme_setting_image_viewer_quality_subtitle": "Ajustar la calidad del visor de detalles de imágenes", "theme_setting_image_viewer_quality_title": "Calidad del visor de imágenes", @@ -337,30 +361,30 @@ "theme_setting_theme_title": "Tema", "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", - "translated_text_options": "Options", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", - "upload_dialog_cancel": "Cancel", - "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", - "upload_dialog_ok": "Upload", - "upload_dialog_title": "Upload Asset", + "translated_text_options": "Opciones", + "trash_page_delete": "Eliminar", + "trash_page_delete_all": "Eliminar todos", + "trash_page_empty_trash_btn": "Vaciar papelera", + "trash_page_empty_trash_dialog_content": "Estás seguro que quieres eliminar los elementos? Estos elementos serán eliminados de Immich permanentemente", + "trash_page_empty_trash_dialog_ok": "Sí", + "trash_page_info": "Los archivos en la papelera serán eliminados automáticamente después de {} días", + "trash_page_no_assets": "No hay elementos en la papelera", + "trash_page_restore": "Restaurar", + "trash_page_restore_all": "Restaurar todos", + "trash_page_select_assets_btn": "Seleccionar elementos", + "trash_page_select_btn": "Seleccionar", + "trash_page_title": "Papelera ({})", + "upload_dialog_cancel": "Cancelar", + "upload_dialog_info": "Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?", + "upload_dialog_ok": "Subir", + "upload_dialog_title": "Subir elementos", "version_announcement_overlay_ack": "Aceptar", "version_announcement_overlay_release_notes": "notas de versión", "version_announcement_overlay_text_1": "Hola amigo, hay una nueva versión de", "version_announcement_overlay_text_2": "por favor, tómate tu tiempo para visitar las ", "version_announcement_overlay_text_3": " y asegúrate de que la configuración de docker-compose y .env estén actualizadas para evitar cualquier error de configuración, especialmente si utilizas WatchTower o cualquier mecanismo que actualice automáticamente la aplicación del servidor.", "version_announcement_overlay_title": "Nueva versión del servidor disponible \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "Quitar de la pila", + "viewer_stack_use_as_main_asset": "Usar como elemento principal", + "viewer_unstack": "Desapilar" } \ No newline at end of file diff --git a/mobile/assets/i18n/es-MX.json b/mobile/assets/i18n/es-MX.json index 55299a262..d140b60ee 100644 --- a/mobile/assets/i18n/es-MX.json +++ b/mobile/assets/i18n/es-MX.json @@ -3,8 +3,8 @@ "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}", "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de recursos encontrados el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.", "advanced_settings_prefer_remote_title": "Preferir imágenes remotas", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados", + "advanced_settings_self_signed_ssl_title": "Permitir certificados autofirmados", "advanced_settings_tile_subtitle": "Configuraciones avanzadas del usuario", "advanced_settings_tile_title": "Avanzado", "advanced_settings_troubleshooting_subtitle": "Habilitar funciones adicionales para solución de problemas", @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Error al cambiar el título del álbum", "album_viewer_appbar_share_leave": "Abandonar álbum ", "album_viewer_appbar_share_remove": "Eliminar del álbum", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Agregar usuarios", "all_people_page_title": "Personas", "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "¿Estás seguro que quieres cerrar sesión?", + "app_bar_signout_dialog_ok": "Sí", + "app_bar_signout_dialog_title": "Cerrar sesión", "archive_page_no_archived_assets": "No se encontraron recursos archivados", "archive_page_title": "Archivo ({})", "asset_list_layout_settings_dynamic_layout_title": "Diseño dinámico", @@ -59,7 +63,7 @@ "backup_controller_page_background_battery_info_title": "Optimizaciones de batería", "backup_controller_page_background_charging": "Solo mientras se carga", "backup_controller_page_background_configure_error": "Error al configurar el servicio en segundo plano", - "backup_controller_page_background_delay": "Retraso en la copia de seguridad de nuevos activos: {}", + "backup_controller_page_background_delay": "Retraso en la copia de seguridad de nuevos elementos: {}", "backup_controller_page_background_description": "Activa el servicio en segundo plano para copiar automáticamente cualquier nuevos archivos sin necesidad de abrir la aplicación.", "backup_controller_page_background_is_off": "La copia de seguridad en segundo plano automática está desactivada", "backup_controller_page_background_is_on": "La copia de seguridad en segundo plano automática está desactivada", @@ -94,11 +98,11 @@ "backup_controller_page_uploading_file_info": "Cargando información del archivo", "backup_err_only_album": "No se puede eliminar el único álbum", "backup_info_card_assets": "archivos", - "backup_manual_cancelled": "Cancelled", - "backup_manual_failed": "Failed", - "backup_manual_in_progress": "Upload already in progress. Try after sometime", - "backup_manual_success": "Success", - "backup_manual_title": "Upload status", + "backup_manual_cancelled": "Cancelado", + "backup_manual_failed": "Fallido", + "backup_manual_in_progress": "Subida en progreso. Espere", + "backup_manual_success": "Éxito", + "backup_manual_title": "Estado de la subida", "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} archivos)", "cache_settings_clear_cache_button": "Borrar caché", "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Uso de caché", "cache_settings_subtitle": "Controla el comportamiento del almacenamiento en caché de la aplicación móvil Immich", "cache_settings_thumbnail_size": "Tamaño de la caché de miniaturas ({} archivos)", + "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local", + "cache_settings_tile_title": "Almacenamiento local", "cache_settings_title": "Configuración de la caché", "change_password_form_confirm_password": "Confirmar Contraseña", - "change_password_form_description": "Hola {firstName} {lastName},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", + "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", "change_password_form_new_password": "Nueva Contraseña", "change_password_form_password_mismatch": "Las contraseñas no coinciden", "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña", @@ -130,10 +136,10 @@ "control_bottom_app_bar_delete": "Eliminar", "control_bottom_app_bar_favorite": "Favorito", "control_bottom_app_bar_share": "Compartir", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_share_to": "Enviar", + "control_bottom_app_bar_stack": "Apilar", "control_bottom_app_bar_unarchive": "Desarchivar", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "Subir", "create_album_page_untitled": "Sin título", "create_shared_album_page_create": "Crear", "create_shared_album_page_share": "Compartir", @@ -148,8 +154,8 @@ "delete_dialog_cancel": "Cancelar", "delete_dialog_ok": "Eliminar", "delete_dialog_title": "Eliminar permanentemente", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_content": "Estás seguro que quieres eliminar este enlace compartido", + "delete_shared_link_dialog_title": "Eliminar enlace compartido", "description_input_hint_text": "Agregar descripción...", "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles", "exif_bottom_sheet_description": "Agregar Descripción...", @@ -164,14 +170,19 @@ "home_page_add_to_album_conflicts": "{added} elementos agregados al álbum {album}.\n{failed} elementos ya existen en el álbum.", "home_page_add_to_album_err_local": "Aún no se pueden agregar recursos locales a álbumes, omitiendo", "home_page_add_to_album_success": "{added} elementos agregados al álbum {album}. ", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Los recursos locales no pueden ser archivados, omitiendo", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Construyendo la línea de tiempo", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Aún no se pueden archivar recursos locales, omitiendo", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.", - "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", + "home_page_share_err_local": "Can not share local assets via link, skipping", + "home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo", "image_viewer_page_state_provider_download_error": "Error de descarga", "image_viewer_page_state_provider_download_success": "Descarga exitosa", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "Error al compartir", "library_page_albums": "Álbumes", "library_page_archive": "Archivo", "library_page_device_albums": "Álbumes en el dispositivo", @@ -179,10 +190,10 @@ "library_page_new_album": "Nuevo álbum", "library_page_sharing": "Compartiendo", "library_page_sort_created": "Creado más recientemente", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "Última modificación", + "library_page_sort_most_recent_photo": "Foto más reciente", "library_page_sort_title": "Título del álbum", - "login_disabled": "Login has been disabled", + "login_disabled": "El inicio de sesión ha sido desactivado", "login_form_api_exception": "Excepción producida por API. Por favor, verifica el URL del servidor e inténtalo de nuevo.", "login_form_button_text": "Iniciar sesión", "login_form_email_hint": "tucorreo@correo.com", @@ -196,7 +207,7 @@ "login_form_failed_get_oauth_server_config": "Error al iniciar sesión con OAuth, verifica la URL del servidor", "login_form_failed_get_oauth_server_disable": "La función de OAuth no está disponible en este servidor", "login_form_failed_login": "Error al iniciar sesión, comprueba la URL del servidor, el correo electrónico y la contraseña", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", + "login_form_handshake_exception": "Hubo un error de verificación del certificado del servidor. Activa el soporte para certificados autofirmados en las preferencias si estás usando un certificado autofirmado", "login_form_label_email": "Correo electrónico", "login_form_label_password": "Contraseña", "login_form_next_button": "Siguiente", @@ -204,24 +215,24 @@ "login_form_save_login": "Permanecer conectado", "login_form_server_empty": "Agrega la URL del servidor.", "login_form_server_error": "No se pudo conectar al servidor.", - "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", - "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_cancel": "Cancel", - "map_location_dialog_yes": "Yes", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", - "map_location_service_disabled_title": "Location Service disabled", - "map_no_assets_in_bounds": "No photos in this area", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", - "map_no_location_permission_title": "Location Permission denied", - "map_settings_dark_mode": "Dark mode", - "map_settings_dialog_cancel": "Cancel", - "map_settings_dialog_save": "Save", - "map_settings_dialog_title": "Map Settings", - "map_settings_include_show_archived": "Include Archived", - "map_settings_only_relative_range": "Date range", - "map_settings_only_show_favorites": "Show Favorite Only", - "map_zoom_to_see_photos": "Zoom out to see photos", + "login_password_changed_error": "Hubo un error actualizando la contraseña", + "login_password_changed_success": "Contraseña cambiado con éxito", + "map_cannot_get_user_location": "No se pudo obtener la posición del usuario", + "map_location_dialog_cancel": "Cancelar", + "map_location_dialog_yes": "Sí", + "map_location_service_disabled_content": "Los servicios de localización deben estar activados para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?", + "map_location_service_disabled_title": "Servicios de localización desactivados", + "map_no_assets_in_bounds": "No hay fotos en esta zona", + "map_no_location_permission_content": "Se necesitan permisos de ubicación para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?", + "map_no_location_permission_title": "Permisos de ubicación denegados", + "map_settings_dark_mode": "Modo oscuro", + "map_settings_dialog_cancel": "Cancelar", + "map_settings_dialog_save": "Guardar", + "map_settings_dialog_title": "Ajustes mapa", + "map_settings_include_show_archived": "Incluir archivados", + "map_settings_only_relative_range": "Rango de fechas", + "map_settings_only_show_favorites": "Mostrar solo favoritas", + "map_zoom_to_see_photos": "Alejar para ver fotos", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Foto en Movimiento", "notification_permission_dialog_cancel": "Cancelar", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} ya no podrá acceder a tus fotos", "partner_page_stop_sharing_title": "¿Dejar de compartir tus fotos?", "partner_page_title": "Compañero", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continuar de todos modos", "permission_onboarding_get_started": "Empezar", "permission_onboarding_go_to_settings": "Ir a configuración", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.", "profile_drawer_app_logs": "Registros", "profile_drawer_client_server_up_to_date": "El cliente y el servidor están actualizados", + "profile_drawer_documentation": "Documentación", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Configuración", "profile_drawer_sign_out": "Cerrar sesión", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "Papelera", "recently_added_page_title": "Recién Agregadas", "search_bar_hint": "Busca tus fotos", "search_page_categories": "Categorías", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Error al crear álbum", "select_user_for_sharing_page_share_suggestions": "Sugerencias", "server_info_box_app_version": "Versión de la Aplicación", + "server_info_box_server_url": "URL del servidor", "server_info_box_server_version": "Versión del Servidor", "setting_image_viewer_help": "El visor de detalles carga primero la miniatura pequeña, luego carga la vista previa de tamaño mediano (si está habilitada), finalmente carga la original (si está habilitada).", "setting_image_viewer_original_subtitle": "Activar para cargar la imagen en resolución original (¡muy grande!). Deshabilitar para reducir el consumo de datos (de red y caché).", @@ -300,35 +315,44 @@ "share_add_photos": "Agregar fotos", "share_add_title": "Agregar un título", "share_create_album": "Crear álbum", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Preparando...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_app_bar_title": "Enlaces compartidos", + "shared_link_create_app_bar_title": "Crear enlace compartido", + "shared_link_create_info": "Cualquier persona con el enlace puede ver las fotos seleccionadas", + "shared_link_create_submit_button": "Crear enlace", + "shared_link_edit_allow_download": "Permitir descargar a usuarios públicos", + "shared_link_edit_allow_upload": "Permitir subir a usuarios públicos", + "shared_link_edit_app_bar_title": "Editar enlace", + "shared_link_edit_change_expiry": "Cambiar fecha de caducidad", + "shared_link_edit_description": "Descripción", + "shared_link_edit_description_hint": "Introduce la descripción del enlace", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Contraseña", + "shared_link_edit_password_hint": "Introduce la contraseña del enlace", + "shared_link_edit_show_meta": "Mostrar metadatos", + "shared_link_edit_submit_button": "Actualizar enlace", + "shared_link_empty": "No tienes enlaces compartidos", + "shared_link_manage_links": "Administrar enlaces compartidos", + "share_done": "Hecho", "share_invite": "Invitar al álbum", "sharing_page_album": "Álbumes compartidos", "sharing_page_description": "Crea álbumes compartidos para compartir fotos y videos con personas de tu red.", "sharing_page_empty_list": "LISTA VACIA", "sharing_silver_appbar_create_shared_album": "Crear álbum compartido", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "Enlaces compartidos", "sharing_silver_appbar_share_partner": "Compartir con compañero", "tab_controller_nav_library": "Biblioteca", "tab_controller_nav_photos": "Fotos", "tab_controller_nav_search": "Buscar", "tab_controller_nav_sharing": "Compartiendo", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los archivos", - "theme_setting_asset_list_tiles_per_row_title": "Número de activos por fila ({})", + "theme_setting_asset_list_tiles_per_row_title": "Número de elementos por fila ({})", "theme_setting_dark_mode_switch": "Modo oscuro", "theme_setting_image_viewer_quality_subtitle": "Ajustar la calidad del visor de detalles de imágenes", "theme_setting_image_viewer_quality_title": "Calidad del visor de imágenes", @@ -337,30 +361,30 @@ "theme_setting_theme_title": "Tema", "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", - "translated_text_options": "Options", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", - "upload_dialog_cancel": "Cancel", - "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", - "upload_dialog_ok": "Upload", - "upload_dialog_title": "Upload Asset", + "translated_text_options": "Opciones", + "trash_page_delete": "Eliminar", + "trash_page_delete_all": "Eliminar todos", + "trash_page_empty_trash_btn": "Vaciar papelera", + "trash_page_empty_trash_dialog_content": "Estás seguro que quieres eliminar los elementos? Estos elementos serán eliminados de Immich permanentemente", + "trash_page_empty_trash_dialog_ok": "Sí", + "trash_page_info": "Los archivos en la papelera serán eliminados automáticamente después de {} días", + "trash_page_no_assets": "No hay elementos en la papelera", + "trash_page_restore": "Restaurar", + "trash_page_restore_all": "Restaurar todos", + "trash_page_select_assets_btn": "Seleccionar elementos", + "trash_page_select_btn": "Seleccionar", + "trash_page_title": "Papelera ({})", + "upload_dialog_cancel": "Cancelar", + "upload_dialog_info": "Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?", + "upload_dialog_ok": "Subir", + "upload_dialog_title": "Subir elementos", "version_announcement_overlay_ack": "Aceptar", "version_announcement_overlay_release_notes": "notas de la versión", "version_announcement_overlay_text_1": "Hola, amigo, hay una nueva versión de", "version_announcement_overlay_text_2": "por favor, tómate tu tiempo para visitar las ", "version_announcement_overlay_text_3": " y asegúrate de que la configuración de docker-compose y .env estén actualizadas para evitar cualquier error de configuración, especialmente si utilizas WatchTower o cualquier mecanismo que actualice automáticamente la aplicación del servidor.", "version_announcement_overlay_title": "Nueva versión del servidor disponible \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "Quitar de la pila", + "viewer_stack_use_as_main_asset": "Usar como elemento principal", + "viewer_unstack": "Desapilar" } \ No newline at end of file diff --git a/mobile/assets/i18n/es-PE.json b/mobile/assets/i18n/es-PE.json index 55299a262..0e03e2fd6 100644 --- a/mobile/assets/i18n/es-PE.json +++ b/mobile/assets/i18n/es-PE.json @@ -3,8 +3,8 @@ "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}", "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de recursos encontrados el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.", "advanced_settings_prefer_remote_title": "Preferir imágenes remotas", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados", + "advanced_settings_self_signed_ssl_title": "Permitir certificados autofirmados", "advanced_settings_tile_subtitle": "Configuraciones avanzadas del usuario", "advanced_settings_tile_title": "Avanzado", "advanced_settings_troubleshooting_subtitle": "Habilitar funciones adicionales para solución de problemas", @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Error al cambiar el título del álbum", "album_viewer_appbar_share_leave": "Abandonar álbum ", "album_viewer_appbar_share_remove": "Eliminar del álbum", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Agregar usuarios", "all_people_page_title": "Personas", "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "¿Estás seguro que quieres cerrar sesión?", + "app_bar_signout_dialog_ok": "Sí", + "app_bar_signout_dialog_title": "Cerrar sesión", "archive_page_no_archived_assets": "No se encontraron recursos archivados", "archive_page_title": "Archivo ({})", "asset_list_layout_settings_dynamic_layout_title": "Diseño dinámico", @@ -59,7 +63,7 @@ "backup_controller_page_background_battery_info_title": "Optimizaciones de batería", "backup_controller_page_background_charging": "Solo mientras se carga", "backup_controller_page_background_configure_error": "Error al configurar el servicio en segundo plano", - "backup_controller_page_background_delay": "Retraso en la copia de seguridad de nuevos activos: {}", + "backup_controller_page_background_delay": "Retraso en la copia de seguridad de nuevos elementos: {}", "backup_controller_page_background_description": "Activa el servicio en segundo plano para copiar automáticamente cualquier nuevos archivos sin necesidad de abrir la aplicación.", "backup_controller_page_background_is_off": "La copia de seguridad en segundo plano automática está desactivada", "backup_controller_page_background_is_on": "La copia de seguridad en segundo plano automática está desactivada", @@ -71,7 +75,7 @@ "backup_controller_page_backup_sub": "Fotos y videos respaldados", "backup_controller_page_cancel": "Cancelar", "backup_controller_page_created": "Creado el: {}", - "backup_controller_page_desc_backup": "Activa la copia de seguridad en primer plano para cargar automáticamente nuevos recursos al servidor al abrir la aplicación.", + "backup_controller_page_desc_backup": "Active la copia de seguridad para cargar automáticamente los nuevos elementos al servidor.", "backup_controller_page_excluded": "Excluido:", "backup_controller_page_failed": "Fallidos ({})", "backup_controller_page_filename": "Nombre del archivo: {} [{}]", @@ -94,11 +98,11 @@ "backup_controller_page_uploading_file_info": "Cargando información del archivo", "backup_err_only_album": "No se puede eliminar el único álbum", "backup_info_card_assets": "archivos", - "backup_manual_cancelled": "Cancelled", - "backup_manual_failed": "Failed", - "backup_manual_in_progress": "Upload already in progress. Try after sometime", - "backup_manual_success": "Success", - "backup_manual_title": "Upload status", + "backup_manual_cancelled": "Cancelado", + "backup_manual_failed": "Fallido", + "backup_manual_in_progress": "Subida en progreso. Espere", + "backup_manual_success": "Éxito", + "backup_manual_title": "Estado de la subida", "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} archivos)", "cache_settings_clear_cache_button": "Borrar caché", "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Uso de caché", "cache_settings_subtitle": "Controla el comportamiento del almacenamiento en caché de la aplicación móvil Immich", "cache_settings_thumbnail_size": "Tamaño de la caché de miniaturas ({} archivos)", + "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local", + "cache_settings_tile_title": "Almacenamiento local", "cache_settings_title": "Configuración de la caché", "change_password_form_confirm_password": "Confirmar Contraseña", - "change_password_form_description": "Hola {firstName} {lastName},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", + "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", "change_password_form_new_password": "Nueva Contraseña", "change_password_form_password_mismatch": "Las contraseñas no coinciden", "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña", @@ -130,10 +136,10 @@ "control_bottom_app_bar_delete": "Eliminar", "control_bottom_app_bar_favorite": "Favorito", "control_bottom_app_bar_share": "Compartir", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_share_to": "Enviar", + "control_bottom_app_bar_stack": "Apilar", "control_bottom_app_bar_unarchive": "Desarchivar", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "Subir", "create_album_page_untitled": "Sin título", "create_shared_album_page_create": "Crear", "create_shared_album_page_share": "Compartir", @@ -148,8 +154,8 @@ "delete_dialog_cancel": "Cancelar", "delete_dialog_ok": "Eliminar", "delete_dialog_title": "Eliminar permanentemente", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_content": "Estás seguro que quieres eliminar este enlace compartido", + "delete_shared_link_dialog_title": "Eliminar enlace compartido", "description_input_hint_text": "Agregar descripción...", "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles", "exif_bottom_sheet_description": "Agregar Descripción...", @@ -164,14 +170,19 @@ "home_page_add_to_album_conflicts": "{added} elementos agregados al álbum {album}.\n{failed} elementos ya existen en el álbum.", "home_page_add_to_album_err_local": "Aún no se pueden agregar recursos locales a álbumes, omitiendo", "home_page_add_to_album_success": "{added} elementos agregados al álbum {album}. ", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Los recursos locales no pueden ser archivados, omitiendo", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Construyendo la línea de tiempo", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Aún no se pueden archivar recursos locales, omitiendo", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.", - "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", + "home_page_share_err_local": "Can not share local assets via link, skipping", + "home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo", "image_viewer_page_state_provider_download_error": "Error de descarga", "image_viewer_page_state_provider_download_success": "Descarga exitosa", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "Error al compartir", "library_page_albums": "Álbumes", "library_page_archive": "Archivo", "library_page_device_albums": "Álbumes en el dispositivo", @@ -179,10 +190,10 @@ "library_page_new_album": "Nuevo álbum", "library_page_sharing": "Compartiendo", "library_page_sort_created": "Creado más recientemente", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "Última modificación", + "library_page_sort_most_recent_photo": "Foto más reciente", "library_page_sort_title": "Título del álbum", - "login_disabled": "Login has been disabled", + "login_disabled": "El inicio de sesión ha sido desactivado", "login_form_api_exception": "Excepción producida por API. Por favor, verifica el URL del servidor e inténtalo de nuevo.", "login_form_button_text": "Iniciar sesión", "login_form_email_hint": "tucorreo@correo.com", @@ -196,7 +207,7 @@ "login_form_failed_get_oauth_server_config": "Error al iniciar sesión con OAuth, verifica la URL del servidor", "login_form_failed_get_oauth_server_disable": "La función de OAuth no está disponible en este servidor", "login_form_failed_login": "Error al iniciar sesión, comprueba la URL del servidor, el correo electrónico y la contraseña", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", + "login_form_handshake_exception": "Hubo un error de verificación del certificado del servidor. Activa el soporte para certificados autofirmados en las preferencias si estás usando un certificado autofirmado", "login_form_label_email": "Correo electrónico", "login_form_label_password": "Contraseña", "login_form_next_button": "Siguiente", @@ -204,24 +215,24 @@ "login_form_save_login": "Permanecer conectado", "login_form_server_empty": "Agrega la URL del servidor.", "login_form_server_error": "No se pudo conectar al servidor.", - "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", - "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_cancel": "Cancel", - "map_location_dialog_yes": "Yes", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", - "map_location_service_disabled_title": "Location Service disabled", - "map_no_assets_in_bounds": "No photos in this area", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", - "map_no_location_permission_title": "Location Permission denied", - "map_settings_dark_mode": "Dark mode", - "map_settings_dialog_cancel": "Cancel", - "map_settings_dialog_save": "Save", - "map_settings_dialog_title": "Map Settings", - "map_settings_include_show_archived": "Include Archived", - "map_settings_only_relative_range": "Date range", - "map_settings_only_show_favorites": "Show Favorite Only", - "map_zoom_to_see_photos": "Zoom out to see photos", + "login_password_changed_error": "Hubo un error actualizando la contraseña", + "login_password_changed_success": "Contraseña cambiado con éxito", + "map_cannot_get_user_location": "No se pudo obtener la posición del usuario", + "map_location_dialog_cancel": "Cancelar", + "map_location_dialog_yes": "Sí", + "map_location_service_disabled_content": "Los servicios de localización deben estar activados para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?", + "map_location_service_disabled_title": "Servicios de localización desactivados", + "map_no_assets_in_bounds": "No hay fotos en esta zona", + "map_no_location_permission_content": "Se necesitan permisos de ubicación para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?", + "map_no_location_permission_title": "Permisos de ubicación denegados", + "map_settings_dark_mode": "Modo oscuro", + "map_settings_dialog_cancel": "Cancelar", + "map_settings_dialog_save": "Guardar", + "map_settings_dialog_title": "Ajustes mapa", + "map_settings_include_show_archived": "Incluir archivados", + "map_settings_only_relative_range": "Rango de fechas", + "map_settings_only_show_favorites": "Mostrar solo favoritas", + "map_zoom_to_see_photos": "Alejar para ver fotos", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Foto en Movimiento", "notification_permission_dialog_cancel": "Cancelar", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} ya no podrá acceder a tus fotos", "partner_page_stop_sharing_title": "¿Dejar de compartir tus fotos?", "partner_page_title": "Compañero", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continuar de todos modos", "permission_onboarding_get_started": "Empezar", "permission_onboarding_go_to_settings": "Ir a configuración", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.", "profile_drawer_app_logs": "Registros", "profile_drawer_client_server_up_to_date": "El cliente y el servidor están actualizados", + "profile_drawer_documentation": "Documentación", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Configuración", "profile_drawer_sign_out": "Cerrar sesión", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "Papelera", "recently_added_page_title": "Recién Agregadas", "search_bar_hint": "Busca tus fotos", "search_page_categories": "Categorías", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Error al crear álbum", "select_user_for_sharing_page_share_suggestions": "Sugerencias", "server_info_box_app_version": "Versión de la Aplicación", + "server_info_box_server_url": "URL del servidor", "server_info_box_server_version": "Versión del Servidor", "setting_image_viewer_help": "El visor de detalles carga primero la miniatura pequeña, luego carga la vista previa de tamaño mediano (si está habilitada), finalmente carga la original (si está habilitada).", "setting_image_viewer_original_subtitle": "Activar para cargar la imagen en resolución original (¡muy grande!). Deshabilitar para reducir el consumo de datos (de red y caché).", @@ -300,35 +315,44 @@ "share_add_photos": "Agregar fotos", "share_add_title": "Agregar un título", "share_create_album": "Crear álbum", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Preparando...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_app_bar_title": "Enlaces compartidos", + "shared_link_create_app_bar_title": "Crear enlace compartido", + "shared_link_create_info": "Cualquier persona con el enlace puede ver las fotos seleccionadas", + "shared_link_create_submit_button": "Crear enlace", + "shared_link_edit_allow_download": "Permitir descargar a usuarios públicos", + "shared_link_edit_allow_upload": "Permitir subir a usuarios públicos", + "shared_link_edit_app_bar_title": "Editar enlace", + "shared_link_edit_change_expiry": "Cambiar fecha de caducidad", + "shared_link_edit_description": "Descripción", + "shared_link_edit_description_hint": "Introduce la descripción del enlace", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Contraseña", + "shared_link_edit_password_hint": "Introduce la contraseña del enlace", + "shared_link_edit_show_meta": "Mostrar metadatos", + "shared_link_edit_submit_button": "Actualizar enlace", + "shared_link_empty": "No tienes enlaces compartidos", + "shared_link_manage_links": "Administrar enlaces compartidos", + "share_done": "Hecho", "share_invite": "Invitar al álbum", "sharing_page_album": "Álbumes compartidos", "sharing_page_description": "Crea álbumes compartidos para compartir fotos y videos con personas de tu red.", "sharing_page_empty_list": "LISTA VACIA", "sharing_silver_appbar_create_shared_album": "Crear álbum compartido", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "Enlaces compartidos", "sharing_silver_appbar_share_partner": "Compartir con compañero", "tab_controller_nav_library": "Biblioteca", "tab_controller_nav_photos": "Fotos", "tab_controller_nav_search": "Buscar", "tab_controller_nav_sharing": "Compartiendo", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los archivos", - "theme_setting_asset_list_tiles_per_row_title": "Número de activos por fila ({})", + "theme_setting_asset_list_tiles_per_row_title": "Número de elementos por fila ({})", "theme_setting_dark_mode_switch": "Modo oscuro", "theme_setting_image_viewer_quality_subtitle": "Ajustar la calidad del visor de detalles de imágenes", "theme_setting_image_viewer_quality_title": "Calidad del visor de imágenes", @@ -337,30 +361,30 @@ "theme_setting_theme_title": "Tema", "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", - "translated_text_options": "Options", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", - "upload_dialog_cancel": "Cancel", - "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", - "upload_dialog_ok": "Upload", - "upload_dialog_title": "Upload Asset", + "translated_text_options": "Opciones", + "trash_page_delete": "Eliminar", + "trash_page_delete_all": "Eliminar todos", + "trash_page_empty_trash_btn": "Vaciar papelera", + "trash_page_empty_trash_dialog_content": "Estás seguro que quieres eliminar los elementos? Estos elementos serán eliminados de Immich permanentemente", + "trash_page_empty_trash_dialog_ok": "Sí", + "trash_page_info": "Los archivos en la papelera serán eliminados automáticamente después de {} días", + "trash_page_no_assets": "No hay elementos en la papelera", + "trash_page_restore": "Restaurar", + "trash_page_restore_all": "Restaurar todos", + "trash_page_select_assets_btn": "Seleccionar elementos", + "trash_page_select_btn": "Seleccionar", + "trash_page_title": "Papelera ({})", + "upload_dialog_cancel": "Cancelar", + "upload_dialog_info": "Quieres hacer una copia de seguridad al servidor de los elementos seleccionados?", + "upload_dialog_ok": "Subir", + "upload_dialog_title": "Subir elementos", "version_announcement_overlay_ack": "Aceptar", "version_announcement_overlay_release_notes": "notas de la versión", "version_announcement_overlay_text_1": "Hola, amigo, hay una nueva versión de", "version_announcement_overlay_text_2": "por favor, tómate tu tiempo para visitar las ", "version_announcement_overlay_text_3": " y asegúrate de que la configuración de docker-compose y .env estén actualizadas para evitar cualquier error de configuración, especialmente si utilizas WatchTower o cualquier mecanismo que actualice automáticamente la aplicación del servidor.", "version_announcement_overlay_title": "Nueva versión del servidor disponible \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "Quitar de la pila", + "viewer_stack_use_as_main_asset": "Usar como elemento principal", + "viewer_unstack": "Desapilar" } \ No newline at end of file diff --git a/mobile/assets/i18n/es-US.json b/mobile/assets/i18n/es-US.json new file mode 100644 index 000000000..1694f5a1f --- /dev/null +++ b/mobile/assets/i18n/es-US.json @@ -0,0 +1,464 @@ +{ + "add_to_album_bottom_sheet_added": "Agregado a {album}", + "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}", + "advanced_settings_log_level_title": "Nivel de registro: {}", + "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de recursos encontrados en el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.", + "advanced_settings_prefer_remote_title": "Preferir imágenes remotas", + "advanced_settings_self_signed_ssl_subtitle": "Omite la verificación del certificado SSL para la URL del servidor. Requerido para certificados autofirmados.", + "advanced_settings_self_signed_ssl_title": "Permitir certificados SSL autofirmados", + "advanced_settings_tile_subtitle": "Configuraciones avanzadas de usuario", + "advanced_settings_tile_title": "Avanzado", + "advanced_settings_troubleshooting_subtitle": "Habilitar funciones adicionales para solución de problemas", + "advanced_settings_troubleshooting_title": "Solución de problemas", + "album_info_card_backup_album_excluded": "EXCLUIDOS", + "album_info_card_backup_album_included": "INCLUIDOS", + "album_thumbnail_card_item": "1 elemento", + "album_thumbnail_card_items": "{} elementos", + "album_thumbnail_card_shared": " · Compartido", + "album_thumbnail_owned": "Propio", + "album_thumbnail_shared_by": "Compartido por {}", + "album_viewer_appbar_share_delete": "Eliminar álbum", + "album_viewer_appbar_share_err_delete": "No se ha podido eliminar el álbum", + "album_viewer_appbar_share_err_leave": "No se ha podido abandonar el álbum", + "album_viewer_appbar_share_err_remove": "Hay problemas para remover los archivos del álbum", + "album_viewer_appbar_share_err_title": "Error al cambiar el título del álbum", + "album_viewer_appbar_share_leave": "Abandonar álbum", + "album_viewer_appbar_share_remove": "Remover del álbum", + "album_viewer_appbar_share_to": "Compartir con", + "album_viewer_page_share_add_users": "Agregar usuarios", + "all_people_page_title": "Personas", + "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "¿Estás seguro de que quieres cerrar sesión?", + "app_bar_signout_dialog_ok": "Sí", + "app_bar_signout_dialog_title": "Cerrar sesión", + "archive_page_no_archived_assets": "No se encontraron recursos archivados", + "archive_page_title": "Archivo ({})", + "asset_list_layout_settings_dynamic_layout_title": "Diseño dinámico", + "asset_list_layout_settings_group_automatically": "Automático", + "asset_list_layout_settings_group_by": "Agrupar recursos por", + "asset_list_layout_settings_group_by_month": "Mes", + "asset_list_layout_settings_group_by_month_day": "Mes + día", + "asset_list_settings_subtitle": "Configuraciones del diseño de la cuadrícula de fotos", + "asset_list_settings_title": "Cuadrícula de fotos", + "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})", + "backup_album_selection_page_albums_tap": "Pulsar para incluir, pulsar dos veces para excluir", + "backup_album_selection_page_assets_scatter": "Los archivos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.", + "backup_album_selection_page_select_albums": "Seleccionar álbumes", + "backup_album_selection_page_selection_info": "Información de la selección", + "backup_album_selection_page_total_assets": "Total de archivos únicos", + "backup_all": "Todos", + "backup_background_service_backup_failed_message": "Error al copiar archivos. Reintentando…", + "backup_background_service_connection_failed_message": "Error al conectar con el servidor. Reintentando…", + "backup_background_service_current_upload_notification": "Subiendo {}", + "backup_background_service_default_notification": "Verificando si hay nuevos archivos…", + "backup_background_service_error_title": "Error de copia de seguridad", + "backup_background_service_in_progress_notification": "Creando copia de seguridad de tus archivos…", + "backup_background_service_upload_failure_notification": "Error al subir {}", + "backup_controller_page_albums": "Álbumes de respaldo", + "backup_controller_page_background_app_refresh_disabled_content": "Activa la actualización en segundo plano de la aplicación en Configuración > General > Actualización en segundo plano para usar la copia de seguridad en segundo plano.", + "backup_controller_page_background_app_refresh_disabled_title": "Actualización en segundo plano desactivada", + "backup_controller_page_background_app_refresh_enable_button_text": "Ir a configuración", + "backup_controller_page_background_battery_info_link": "Muéstrame cómo", + "backup_controller_page_background_battery_info_message": "Para obtener la mejor experiencia de copia de seguridad en segundo plano, desactiva cualquier optimización de batería que restrinja la actividad en segundo plano para Immich.\n\nDado que esto es específico en cada dispositivo, busca la información necesaria del fabricante de tu dispositivo.", + "backup_controller_page_background_battery_info_ok": "Ok", + "backup_controller_page_background_battery_info_title": "Optimizaciones de batería", + "backup_controller_page_background_charging": "Sólo mientras se carga", + "backup_controller_page_background_configure_error": "Error al configurar el servicio en segundo plano", + "backup_controller_page_background_delay": "Retraso en la copia de seguridad de nuevos recursos: {}", + "backup_controller_page_background_description": "Activa el servicio en segundo plano para copiar automáticamente cualquier nuevo recurso sin necesidad de abrir la aplicación", + "backup_controller_page_background_is_off": "La copia de seguridad en segundo plano automática está desactivada", + "backup_controller_page_background_is_on": "La copia de seguridad en segundo plano automática está activada", + "backup_controller_page_background_turn_off": "Desactivar el servicio en segundo plano", + "backup_controller_page_background_turn_on": "Activar el servicio en segundo plano", + "backup_controller_page_background_wifi": "Sólo en WiFi", + "backup_controller_page_backup": "Respaldo", + "backup_controller_page_backup_selected": "Seleccionado: ", + "backup_controller_page_backup_sub": "Fotos y videos respaldados", + "backup_controller_page_cancel": "Cancelar", + "backup_controller_page_created": "Creado el: {}", + "backup_controller_page_desc_backup": "Activa la copia de seguridad en primer plano para subir automáticamente nuevos recursos al servidor al abrir la aplicación.", + "backup_controller_page_excluded": "Excluído: ", + "backup_controller_page_failed": "Fallidos ({})", + "backup_controller_page_filename": "Nombre del archivo: {} [{}]", + "backup_controller_page_id": "ID: {}", + "backup_controller_page_info": "Información del respaldo", + "backup_controller_page_none_selected": "Ninguno seleccionado", + "backup_controller_page_remainder": "Restante", + "backup_controller_page_remainder_sub": "Fotos y videos restantes para hacer una copia de seguridad de la selección", + "backup_controller_page_select": "Seleccionar", + "backup_controller_page_server_storage": "Almacenamiento del servidor", + "backup_controller_page_start_backup": "Iniciar respaldo", + "backup_controller_page_status_off": "La copia de seguridad automática en primer plano está desactivada", + "backup_controller_page_status_on": "La copia de seguridad automática en primer plano está activada", + "backup_controller_page_storage_format": "{} de {} usado", + "backup_controller_page_to_backup": "Álbumes a respaldar", + "backup_controller_page_total": "Total", + "backup_controller_page_total_sub": "Todas las fotos y videos únicos de los álbumes seleccionados", + "backup_controller_page_turn_off": "Desactivar la copia de seguridad en primer plano", + "backup_controller_page_turn_on": "Activar la copia de seguridad en primer plano", + "backup_controller_page_uploading_file_info": "Subiendo información del archivo", + "backup_err_only_album": "No se puede eliminar el único álbum", + "backup_info_card_assets": "recursos", + "backup_manual_cancelled": "Cancelado", + "backup_manual_failed": "Fallido", + "backup_manual_in_progress": "Subida ya en progreso. Inténtalo después de un tiempo", + "backup_manual_success": "Exitoso", + "backup_manual_title": "Estado de subida", + "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} recursos)", + "cache_settings_clear_cache_button": "Borrar caché", + "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.", + "cache_settings_duplicated_assets_clear_button": "BORRAR", + "cache_settings_duplicated_assets_subtitle": "Fotos y videos que son ignorados por la aplicación", + "cache_settings_duplicated_assets_title": "Recursos duplicados ({})", + "cache_settings_image_cache_size": "Tamaño de la caché de imágenes ({} recursos)", + "cache_settings_statistics_album": "Miniaturas de la biblioteca", + "cache_settings_statistics_assets": "{} recursos ({})", + "cache_settings_statistics_full": "Imágenes completas", + "cache_settings_statistics_shared": "Miniaturas de álbumes compartidos", + "cache_settings_statistics_thumbnail": "Miniaturas", + "cache_settings_statistics_title": "Uso de caché", + "cache_settings_subtitle": "Controla el comportamiento de la caché de la aplicación móvil Immich", + "cache_settings_thumbnail_size": "Tamaño de la caché de miniaturas ({} recursos)", + "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local", + "cache_settings_tile_title": "Almacenamiento local", + "cache_settings_title": "Configuración de la caché", + "change_password_form_confirm_password": "Confirmar Contraseña", + "change_password_form_description": "Hola {name},\n\nÉsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", + "change_password_form_new_password": "Nueva Contraseña", + "change_password_form_password_mismatch": "Las contraseñas no coinciden", + "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña", + "common_add_to_album": "Agregar al álbum", + "common_change_password": "Cambiar Contraseña", + "common_create_new_album": "Crear nuevo álbum", + "common_server_error": "Por favor, verifica tu conexión de red, asegúrate de que el servidor esté accesible y las versiones de la aplicación y del servidor sean compatibles.", + "common_shared": "Compartido", + "control_bottom_app_bar_add_to_album": "Agregar al álbum", + "control_bottom_app_bar_album_info": "{} elementos", + "control_bottom_app_bar_album_info_shared": "{} elementos · Compartido", + "control_bottom_app_bar_archive": "Archivar", + "control_bottom_app_bar_create_new_album": "Crear nuevo álbum", + "control_bottom_app_bar_delete": "Eliminar", + "control_bottom_app_bar_favorite": "Favorito", + "control_bottom_app_bar_share": "Compartir", + "control_bottom_app_bar_share_to": "Compartir con", + "control_bottom_app_bar_stack": "Apilar", + "control_bottom_app_bar_unarchive": "Desarchivar", + "control_bottom_app_bar_upload": "Subir", + "create_album_page_untitled": "Sin título", + "create_shared_album_page_create": "Crear", + "create_shared_album_page_share": "Compartir", + "create_shared_album_page_share_add_assets": "AGREGAR RECURSOS", + "create_shared_album_page_share_select_photos": "Seleccionar fotos", + "curated_location_page_title": "Lugares", + "curated_object_page_title": "Objetos", + "daily_title_text_date": "E, dd MMM", + "daily_title_text_date_year": "E, dd de MMM, yyyy", + "date_format": "E d, LLL y • h:mm a", + "delete_dialog_alert": "Estos elementos se eliminarán permanentemente de Immich y de tu dispositivo", + "delete_dialog_cancel": "Cancelar", + "delete_dialog_ok": "Eliminar", + "delete_dialog_title": "Eliminar permanentemente", + "delete_shared_link_dialog_content": "¿Estás seguro de que quieres eliminar este enlace compartido?", + "delete_shared_link_dialog_title": "Eliminar enlace compartido", + "description_input_hint_text": "Agregar descripción...", + "description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles", + "exif_bottom_sheet_description": "Agregar Descripción...", + "exif_bottom_sheet_details": "DETALLES", + "exif_bottom_sheet_location": "UBICACIÓN", + "experimental_settings_new_asset_list_subtitle": "Trabajo en progreso", + "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental", + "experimental_settings_subtitle": "¡Úsalo bajo tu propio riesgo!", + "experimental_settings_title": "Experimental", + "favorites_page_no_favorites": "No se encontraron recursos marcados como favoritos", + "favorites_page_title": "Favoritos", + "home_page_add_to_album_conflicts": "{added} recursos agregados al álbum {album}.\n{failed} recursos ya existen en el álbum.", + "home_page_add_to_album_err_local": "Aún no se pueden agregar recursos locales a álbumes, omitiendo", + "home_page_add_to_album_success": "{added} recursos agregados al álbum {album}.", + "home_page_archive_err_local": "Aún no se pueden archivar recursos locales, omitiendo", + "home_page_building_timeline": "Construyendo la línea de tiempo", + "home_page_favorite_err_local": "Aún no se pueden marcar recursos locales como favoritos, omitiendo", + "home_page_first_time_notice": "Si ésta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.", + "home_page_share_err_local": "No se pueden compartir recursos locales a través de enlaces, omitiendo", + "home_page_upload_err_limit": "Sólo se pueden subir un máximo de 30 recursos a la vez, omitiendo", + "home_page_favorite_err_partner": "Aún no se pueden marcar recursos de compañeros como favoritos, omitiendo", + "home_page_album_err_partner": "Aún no se pueden agregar recursos de compañeros a un álbum, omitiendo", + "home_page_archive_err_partner": "Aún no se pueden archivar recursos de compañeros, omitiendo", + "home_page_delete_err_partner": "Aún no se pueden eliminar recursos de compañeros, omitiendo", + "image_viewer_page_state_provider_download_error": "Error de descarga", + "image_viewer_page_state_provider_download_success": "Descarga exitosa", + "image_viewer_page_state_provider_share_error": "Error al compartir", + "library_page_albums": "Álbumes", + "library_page_archive": "Archivo", + "library_page_device_albums": "Álbumes en el dispositivo", + "library_page_favorites": "Favoritos", + "library_page_new_album": "Nuevo álbum", + "library_page_sharing": "Compartiendo", + "library_page_sort_created": "Creado más recientemente", + "library_page_sort_last_modified": "Modificado más recientemente", + "library_page_sort_most_recent_photo": "Foto más reciente", + "library_page_sort_title": "Título del álbum", + "login_disabled": "El inicio de sesión ha sido deshabilitado", + "login_form_api_exception": "Excepción de API. Por favor, verifica la URL del servidor e inténtalo de nuevo.", + "login_form_back_button_text": "Volver", + "login_form_button_text": "Iniciar sesión", + "login_form_email_hint": "tucorreo@correo.com", + "login_form_endpoint_hint": "http://ip-de-tu-servidor:puerto/api", + "login_form_endpoint_url": "URL del servidor", + "login_form_err_http": "Por favor, especifique http:// o https://", + "login_form_err_invalid_email": "Correo electrónico inválido", + "login_form_err_invalid_url": "URL inválido", + "login_form_err_leading_whitespace": "Espacio en blanco inicial", + "login_form_err_trailing_whitespace": "Espacio en blanco al final", + "login_form_failed_get_oauth_server_config": "Error al iniciar sesión con OAuth, verifica la URL del servidor", + "login_form_failed_get_oauth_server_disable": "La función de OAuth no está disponible en este servidor", + "login_form_failed_login": "Error al iniciar sesión, comprueba la URL del servidor, el correo electrónico y la contraseña", + "login_form_handshake_exception": "Hubo una excepción de handshake con el servidor. Habilita el soporte de certificado autofirmado en la configuración si estás usando un certificado autofirmado.", + "login_form_label_email": "Correo electrónico", + "login_form_label_password": "Contraseña", + "login_form_next_button": "Siguiente", + "login_form_password_hint": "contraseña", + "login_form_save_login": "Permanecer conectado", + "login_form_server_empty": "Introduce la URL del servidor.", + "login_form_server_error": "No se pudo conectar al servidor.", + "login_password_changed_error": "Hubo un error al actualizar tu contraseña", + "login_password_changed_success": "Contraseña actualizada exitosamente", + "map_assets_in_bounds": { + "one": "{} foto", + "other": "{} fotos" + }, + "map_cannot_get_user_location": "No se puede obtener la ubicación del usuario", + "map_location_dialog_cancel": "Cancelar", + "map_location_dialog_yes": "Sí", + "map_location_service_disabled_content": "El servicio de ubicación debe estar habilitado para mostrar recursos desde tu ubicación actual. ¿Quieres habilitarlo ahora?", + "map_location_service_disabled_title": "Servicio de ubicación deshabilitado", + "map_no_assets_in_bounds": "No hay fotos en esta área", + "map_no_location_permission_content": "Se necesita permiso de ubicación para mostrar recursos desde tu ubicación actual. ¿Quieres permitirlo ahora?", + "map_no_location_permission_title": "Permiso de ubicación denegado", + "map_settings_dark_mode": "Modo oscuro", + "map_settings_date_range_option_all": "Todo", + "map_settings_date_range_option_days": { + "one": "Últimas 24 horas", + "other": "Últimos {} días" + }, + "map_settings_date_range_option_years": { + "one": "Último año", + "other": "Últimos {} años" + }, + "map_settings_dialog_cancel": "Cancelar", + "map_settings_dialog_save": "Guardar", + "map_settings_dialog_title": "Configuración del mapa", + "map_settings_include_show_archived": "Incluir archivados", + "map_settings_only_relative_range": "Rango de fechas", + "map_settings_only_show_favorites": "Mostrar sólo favoritos", + "map_zoom_to_see_photos": "Aleja el mapa para ver las fotos", + "monthly_title_text_date_format": "MMMM y", + "motion_photos_page_title": "Fotos en movimiento", + "notification_permission_dialog_cancel": "Cancelar", + "notification_permission_dialog_content": "Para activar las notificaciones, ve a Configuración y selecciona permitir.", + "notification_permission_dialog_settings": "Configuración", + "notification_permission_list_tile_content": "Concede permiso para activar las notificaciones.", + "notification_permission_list_tile_enable_button": "Activar notificaciones", + "notification_permission_list_tile_title": "Permisos de notificación", + "partner_page_add_partner": "Agregar compañero", + "partner_page_empty_message": "Tus fotos aún no se han compartido con ningún compañero.", + "partner_page_no_more_users": "No hay más usuarios para agregar", + "partner_page_partner_add_failed": "Error al agregar compañero", + "partner_page_select_partner": "Seleccionar compañero", + "partner_page_shared_to_title": "Compartido con", + "partner_page_stop_sharing_content": "{} ya no podrá acceder a tus fotos", + "partner_page_stop_sharing_title": "¿Dejar de compartir tus fotos?", + "partner_page_title": "Compañero", + "permission_onboarding_back": "Volver", + "permission_onboarding_continue_anyway": "Continuar de todos modos", + "permission_onboarding_get_started": "Empezar", + "permission_onboarding_go_to_settings": "Ir a configuración", + "permission_onboarding_grant_permission": "Conceder permiso", + "permission_onboarding_log_out": "Cerrar sesión", + "permission_onboarding_permission_denied": "Permiso denegado. Para usar Immich, concede permisos de fotos y videos en Configuración.", + "permission_onboarding_permission_granted": "¡Permiso concedido! Todo listo.", + "permission_onboarding_permission_limited": "Permiso limitado. Para permitir que Immich haga copia de seguridad y gestione toda tu colección de galería, concede permisos de fotos y videos en Configuración.", + "permission_onboarding_request": "Immich requiere permiso para ver tus fotos y videos.", + "profile_drawer_app_logs": "Registros", + "profile_drawer_client_out_of_date_major": "La aplicación móvil está desactualizada. Actualiza a la última versión mayor.", + "profile_drawer_client_out_of_date_minor": "La aplicación móvil está desactualizada. Actualiza a la última versión menor.", + "profile_drawer_client_server_up_to_date": "El cliente y el servidor están actualizados", + "profile_drawer_documentation": "Documentación", + "profile_drawer_github": "GitHub", + "profile_drawer_server_out_of_date_major": "El servidor está desactualizado. Actualiza a la última versión mayor.", + "profile_drawer_server_out_of_date_minor": "El servidor está desactualizado. Actualiza a la última versión menor.", + "profile_drawer_settings": "Configuración", + "profile_drawer_sign_out": "Cerrar sesión", + "profile_drawer_trash": "Papelera", + "recently_added_page_title": "Recién Agregados", + "search_bar_hint": "Busca tus fotos", + "search_page_categories": "Categorías", + "search_page_favorites": "Favoritos", + "search_page_motion_photos": "Fotos en movimiento", + "search_page_no_objects": "No hay información de objetos disponible", + "search_page_no_places": "No hay información de lugares disponible", + "search_page_people": "Personas", + "search_page_person_add_name_dialog_cancel": "Cancelar", + "search_page_person_add_name_dialog_hint": "Nombre", + "search_page_person_add_name_dialog_save": "Guardar", + "search_page_person_add_name_dialog_title": "Agregar nombre", + "search_page_person_add_name_subtitle": "Encuéntralos rápidamente por nombre", + "search_page_person_add_name_title": "Agregar un nombre", + "search_page_person_edit_name": "Editar nombre", + "search_page_places": "Lugares", + "search_page_recently_added": "Recién agregados", + "search_page_screenshots": "Capturas de pantalla", + "search_page_selfies": "Selfies", + "search_page_things": "Cosas", + "search_page_videos": "Videos", + "search_page_view_all_button": "Ver todo", + "search_page_your_activity": "Tu actividad", + "search_page_your_map": "Tu mapa", + "search_result_page_new_search_hint": "Nueva búsqueda", + "search_suggestion_list_smart_search_hint_1": "La búsqueda inteligente está habilitada por defecto, para buscar metadatos utiliza la sintaxis ", + "search_suggestion_list_smart_search_hint_2": "m:tu-término-de-búsqueda", + "select_additional_user_for_sharing_page_suggestions": "Sugerencias", + "select_user_for_sharing_page_err_album": "Error al crear álbum", + "select_user_for_sharing_page_share_suggestions": "Sugerencias", + "server_info_box_app_version": "Versión de la Aplicación", + "server_info_box_server_url": "URL del Servidor", + "server_info_box_server_version": "Versión del Servidor", + "server_info_box_latest_release": "Última versión", + "setting_image_viewer_help": "El visor de detalles carga primero la miniatura pequeña, luego carga la vista previa de tamaño mediano (si está habilitada), finalmente carga la original (si está habilitada).", + "setting_image_viewer_original_subtitle": "Activar para cargar la imagen en resolución original (¡muy grande!). Deshabilitar para reducir el consumo de datos (de red y caché).", + "setting_image_viewer_original_title": "Cargar imagen original", + "setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Deshabilitar para cargar directamente la imagen original o usar una miniatura.", + "setting_image_viewer_preview_title": "Cargar imagen de previsualización", + "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}", + "setting_notifications_notify_hours": "{} horas", + "setting_notifications_notify_immediately": "inmediatamente", + "setting_notifications_notify_minutes": "{} minutos", + "setting_notifications_notify_never": "nunca", + "setting_notifications_notify_seconds": "{} segundos", + "setting_notifications_single_progress_subtitle": "Información detallada del progreso de subida de cada recurso", + "setting_notifications_single_progress_title": "Mostrar progreso detallado de copia de seguridad en segundo plano", + "setting_notifications_subtitle": "Ajusta tus preferencias de notificación", + "setting_notifications_title": "Notificaciones", + "setting_notifications_total_progress_subtitle": "Progreso general de subida (recursos completados/totales)", + "setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano", + "setting_pages_app_bar_settings": "Configuración", + "settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste", + "share_add": "Agregar", + "share_add_photos": "Agregar fotos", + "share_add_title": "Agregar un título", + "share_create_album": "Crear álbum", + "shared_album_activities_input_disable": "Los comentarios están deshabilitados", + "shared_album_activities_input_hint": "Di algo", + "shared_album_activity_remove_content": "¿Quieres eliminar esta actividad?", + "shared_album_activity_remove_title": "Eliminar actividad", + "shared_album_activity_setting_subtitle": "Permitir que otros respondan", + "shared_album_activity_setting_title": "Comentarios y me gusta", + "shared_album_section_people_action_error": "Error al dejar/remover del álbum", + "shared_album_section_people_action_leave": "Dejar álbum", + "shared_album_section_people_action_remove_user": "Remover usuario del álbum", + "shared_album_section_people_owner_label": "Dueño", + "shared_album_section_people_title": "PERSONAS", + "share_dialog_preparing": "Preparando...", + "shared_link_app_bar_title": "Enlaces compartidos", + "shared_link_clipboard_copied_massage": "Copiado al portapapeles", + "shared_link_clipboard_text": "Enlace: {}\nContraseña: {}", + "shared_link_create_app_bar_title": "Crear enlace para compartir", + "shared_link_create_error": "Error al crear enlace compartido", + "shared_link_create_info": "Permitir que cualquiera con el enlace vea la(s) foto(s) seleccionada(s)", + "shared_link_create_submit_button": "Crear enlace", + "shared_link_edit_allow_download": "Permitir que el usuario público pueda descargar", + "shared_link_edit_allow_upload": "Permitir que el usuario público pueda subir", + "shared_link_edit_app_bar_title": "Editar enlace", + "shared_link_edit_change_expiry": "Cambiar tiempo de expiración", + "shared_link_edit_description": "Descripción", + "shared_link_edit_description_hint": "Introduce la descripción del enlace", + "shared_link_edit_expire_after": "Expirar después de", + "shared_link_edit_expire_after_option_days": { + "one": "{} día", + "other": "{} días" + }, + "shared_link_edit_expire_after_option_hours": { + "one": "{} hora", + "other": "{} horas" + }, + "shared_link_edit_expire_after_option_minutes": { + "one": "{} minuto", + "other": "{} minutos" + }, + "shared_link_edit_expire_after_option_never": "Nunca", + "shared_link_edit_password": "Contraseña", + "shared_link_edit_password_hint": "Introduce la contraseña del enlace", + "shared_link_edit_show_meta": "Mostrar metadatos", + "shared_link_edit_submit_button": "Actualizar enlace", + "shared_link_empty": "No tienes ningún enlace compartido", + "shared_link_error_server_url_fetch": "No se puede obtener la URL del servidor", + "shared_link_expired": "Expirado", + "shared_link_expires_days": { + "one": "Expira en {} día", + "other": "Expira en {} días" + }, + "shared_link_expires_hours": { + "one": "Expira en {} hora", + "other": "Expira en {} horas" + }, + "shared_link_expires_minutes": { + "one": "Expira en {} minuto", + "other": "Expira en {} minutos" + }, + "shared_link_expires_seconds": { + "one": "Expira en {} segundo", + "other": "Expira en {} segundos" + }, + "shared_link_expires_never": "Sin expiración", + "shared_link_info_chip_download": "Descargar", + "shared_link_info_chip_metadata": "EXIF", + "shared_link_info_chip_upload": "Subir", + "shared_link_manage_links": "Administrar enlaces compartidos", + "share_done": "Hecho", + "share_invite": "Invitar al álbum", + "sharing_page_album": "Álbumes compartidos", + "sharing_page_description": "Crea álbumes compartidos para compartir fotos y videos con personas de tu red.", + "sharing_page_empty_list": "LISTA VACÍA", + "sharing_silver_appbar_create_shared_album": "Crear álbum compartido", + "sharing_silver_appbar_shared_links": "Enlaces compartidos", + "sharing_silver_appbar_share_partner": "Compartir con compañero", + "tab_controller_nav_library": "Biblioteca", + "tab_controller_nav_photos": "Fotos", + "tab_controller_nav_search": "Buscar", + "tab_controller_nav_sharing": "Compartidos", + "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los recursos", + "theme_setting_asset_list_tiles_per_row_title": "Número de recursos por fila ({})", + "theme_setting_dark_mode_switch": "Modo oscuro", + "theme_setting_image_viewer_quality_subtitle": "Ajustar la calidad del visor de detalles de imágenes", + "theme_setting_image_viewer_quality_title": "Calidad del visor de imágenes", + "theme_setting_system_theme_switch": "Automático (seguir ajuste del sistema)", + "theme_setting_theme_subtitle": "Elige la configuración del tema de la aplicación", + "theme_setting_theme_title": "Tema", + "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", + "translated_text_options": "Opciones", + "trash_page_delete": "Eliminar", + "trash_page_delete_all": "Eliminar todos", + "trash_page_empty_trash_btn": "Vaciar papelera", + "trash_page_empty_trash_dialog_content": "¿Quieres vaciar los recursos de la papelera? Estos elementos serán eliminados permanentemente de Immich", + "trash_page_empty_trash_dialog_ok": "Ok", + "trash_page_info": "Los elementos en la papelera serán borrados permanentemente luego de {} días", + "trash_page_no_assets": "No hay recursos en la papelera", + "trash_page_restore": "Restaurar", + "trash_page_restore_all": "Restaurar todos", + "trash_page_select_assets_btn": "Seleccionar recursos", + "trash_page_select_btn": "Seleccionar", + "trash_page_title": "Papelera ({})", + "upload_dialog_cancel": "Cancelar", + "upload_dialog_info": "¿Quieres respaldar los recursos seleccionados en el servidor?", + "upload_dialog_ok": "Subir", + "upload_dialog_title": "Subir recurso", + "version_announcement_overlay_ack": "Aceptar", + "version_announcement_overlay_release_notes": "notas de la versión", + "version_announcement_overlay_text_1": "Hola, amigo, hay una nueva versión de", + "version_announcement_overlay_text_2": "por favor, tómate tu tiempo para visitar las ", + "version_announcement_overlay_text_3": " y asegúrate de que la configuración de docker-compose y .env estén actualizadas para evitar cualquier error de configuración, especialmente si utilizas WatchTower o cualquier mecanismo que actualice automáticamente la aplicación del servidor.", + "version_announcement_overlay_title": "Nueva versión del servidor disponible \uD83C\uDF89", + "viewer_remove_from_stack": "Eliminar de la pila", + "viewer_stack_use_as_main_asset": "Utilizar como recurso principal", + "viewer_unstack": "Desapilar" +} diff --git a/mobile/assets/i18n/fi-FI.json b/mobile/assets/i18n/fi-FI.json index 1aa8a926a..d746f99c1 100644 --- a/mobile/assets/i18n/fi-FI.json +++ b/mobile/assets/i18n/fi-FI.json @@ -3,8 +3,8 @@ "add_to_album_bottom_sheet_already_exists": "Kohde on jo albumissa {album}", "advanced_settings_prefer_remote_subtitle": "Jotkut laitteet ovat erittäin hitaita lataamaan esikatselukuvia laitteen kohteista. Aktivoi tämä asetus käyttääksesi etäkuvia.", "advanced_settings_prefer_remote_title": "Suosi etäkuvia", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_self_signed_ssl_subtitle": "Ohita SSL sertifikaattivarmennus palvelimen päätepisteellä. Vaaditaan self-signed -sertifikaateissa.", + "advanced_settings_self_signed_ssl_title": "Salli self-signed SSL -sertifikaatit", "advanced_settings_tile_subtitle": "Edistyneen käyttäjän asetukset", "advanced_settings_tile_title": "Edistyneet", "advanced_settings_troubleshooting_subtitle": "Kytke vianetsinnän lisäominaisuudet päälle", @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Albumin nimen muuttaminen epäonnistui", "album_viewer_appbar_share_leave": "Poistu albumista", "album_viewer_appbar_share_remove": "Poista albumista", + "album_viewer_appbar_share_to": "Jaa", "album_viewer_page_share_add_users": "Lisää käyttäjiä", "all_people_page_title": "Ihmiset", "all_videos_page_title": "Videot", + "app_bar_signout_dialog_content": "Haluatko varmasti kirjautua ulos?", + "app_bar_signout_dialog_ok": "Kyllä", + "app_bar_signout_dialog_title": "Kirjaudu ulos", "archive_page_no_archived_assets": "Arkistoituja kohteita ei löytynyt", "archive_page_title": "Arkisto ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynaaminen asetelma", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Välimuistin käyttö", "cache_settings_subtitle": "Hallitse Immich-mobiilisovelluksen välimuistin käyttöä", "cache_settings_thumbnail_size": "Esikatselukuvien välimuistin koko ({} kohdetta)", + "cache_settings_tile_subtitle": "Hallitse paikallista tallenustilaa", + "cache_settings_tile_title": "Paikallinen tallennustila", "cache_settings_title": "Välimuistin asetukset", "change_password_form_confirm_password": "Vahvista salasana", - "change_password_form_description": "Hei {firstName} {lastName},\n\nTämä on joko ensimmäinen kirjautumisesi järjestelmään tai salasanan vaihtaminen vaihtaminen on pakotettu. Ole hyvä ja syötä uusi salasana alle.", + "change_password_form_description": "Hei {name},\n\nTämä on joko ensimmäinen kirjautumisesi järjestelmään tai salasanan vaihtaminen vaihtaminen on pakotettu. Ole hyvä ja syötä uusi salasana alle.", "change_password_form_new_password": "Uusi salasana", "change_password_form_password_mismatch": "Salasanat eivät täsmää", "change_password_form_reenter_new_password": "Uusi salasana uudelleen", @@ -130,10 +136,10 @@ "control_bottom_app_bar_delete": "Poista", "control_bottom_app_bar_favorite": "Suosikki", "control_bottom_app_bar_share": "Jaa", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_share_to": "Jaa", + "control_bottom_app_bar_stack": "Pinoa", "control_bottom_app_bar_unarchive": "Palauta arkistosta", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "Siirrä palvelimelle", "create_album_page_untitled": "Nimetön", "create_shared_album_page_create": "Luo", "create_shared_album_page_share": "Jaa", @@ -148,8 +154,8 @@ "delete_dialog_cancel": "Peruuta", "delete_dialog_ok": "Poista", "delete_dialog_title": "Poista pysyvästi", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_content": "Oletko varma, että haluat poistaa jaetun linkin?", + "delete_shared_link_dialog_title": "Poista jaettu linkki", "description_input_hint_text": "Lisää kuvaus...", "description_input_submit_error": "Virhe kuvauksen päivittämisessä, tarkista lisätiedot lokista", "exif_bottom_sheet_description": "Lisää kuvaus…", @@ -164,14 +170,19 @@ "home_page_add_to_album_conflicts": "Lisätty {added} kohdetta albumiin {album}. {failed} kohdetta on jo albumissa.", "home_page_add_to_album_err_local": "Paikallisten kohteiden lisääminen albumeihin ei ole mahdollista, ohitetaan", "home_page_add_to_album_success": "Lisätty {added} kohdetta albumiin {album}.", + "home_page_album_err_partner": "Kumppanin kohteita ei voi vielä lisätä albumiin. Hypätään yli", "home_page_archive_err_local": "Paikallisten kohteiden arkistointi ei ole mahdollista, ohitetaan", + "home_page_archive_err_partner": "Kumppanin kohteita ei voi arkistoida. Hypätään yli", "home_page_building_timeline": "Rakennetaan aikajanaa", + "home_page_delete_err_partner": "Kumppanin kohteita ei voi poistaa.Hypätään yli", "home_page_favorite_err_local": "Paikallisten kohteiden lisääminen suosikkeihin ei ole mahdollista, ohitetaan", + "home_page_favorite_err_partner": "Kumppanin kohteita ei voi vielä merkitä suosikiksi. Hypätään yli", "home_page_first_time_notice": "Jos käytät sovellusta ensimmäistä kertaa, muista valita varmuuskopioitavat albumi(t), jotta aikajanalla voi olla kuvia ja videoita.", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Voit lähettää palvelimelle enintään 30 kohdetta kerrallaan, ohitetaan", "image_viewer_page_state_provider_download_error": "Lataus epäonnistui", "image_viewer_page_state_provider_download_success": "Lataus onnistui", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "Jakovirhe", "library_page_albums": "Albumit", "library_page_archive": "Arkisto", "library_page_device_albums": "Laitteen albumit", @@ -179,8 +190,8 @@ "library_page_new_album": "Uusi albumi", "library_page_sharing": "Jakaminen", "library_page_sort_created": "Viimeisin luotu", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "Viimeksi muokattu", + "library_page_sort_most_recent_photo": "Viimeisin kuva", "library_page_sort_title": "Albumin otsikko", "login_disabled": "Kirjautuminen on poistettu käytöstä", "login_form_api_exception": "API-virhe. Tarkista palvelimen URL-osoite ja yritä uudelleen.", @@ -196,7 +207,7 @@ "login_form_failed_get_oauth_server_config": "Virhe kirjauduttaessa OAuth:lla, tarkista palvelimen URL", "login_form_failed_get_oauth_server_disable": "OAuth-ominaisuus ei ole käytössä tällä palvelimella", "login_form_failed_login": "Virhe kirjautumisessa. Tarkista palvelimen URL, sähköpostiosoite ja salasana.", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", + "login_form_handshake_exception": "Tapahtui poikkeus kättelyssä palvelimen kanssa. Kytke päälle self-signed -sertifikaattituki asetuksista, mikäli käytät self-signed -sertifikaatteja.", "login_form_label_email": "Sähköposti", "login_form_label_password": "Salasana", "login_form_next_button": "Seuraava", @@ -204,24 +215,24 @@ "login_form_save_login": "Pysy kirjautuneena", "login_form_server_empty": "Syötä palvelimen URL-osoite.", "login_form_server_error": "Palvelimeen ei saatu yhteyttä.", - "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", - "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_cancel": "Cancel", - "map_location_dialog_yes": "Yes", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", - "map_location_service_disabled_title": "Location Service disabled", - "map_no_assets_in_bounds": "No photos in this area", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", - "map_no_location_permission_title": "Location Permission denied", - "map_settings_dark_mode": "Dark mode", - "map_settings_dialog_cancel": "Cancel", - "map_settings_dialog_save": "Save", - "map_settings_dialog_title": "Map Settings", - "map_settings_include_show_archived": "Include Archived", - "map_settings_only_relative_range": "Date range", - "map_settings_only_show_favorites": "Show Favorite Only", - "map_zoom_to_see_photos": "Zoom out to see photos", + "login_password_changed_error": "Salasanan päivityksessä tapahtui virhe", + "login_password_changed_success": "Salasan päivitetty onnistuneesti", + "map_cannot_get_user_location": "Käyttäjän sijaintia ei voitu määrittää", + "map_location_dialog_cancel": "Peruuta", + "map_location_dialog_yes": "Kyllä", + "map_location_service_disabled_content": "Paikannuspalvelun pitää olla päälle kytkettynä, jotta nykyisen sijaintisi kohteita voidaan näyttää. Haluatko kytkeä sen päälle?", + "map_location_service_disabled_title": "Paikannuspalvelu pois päältä", + "map_no_assets_in_bounds": "Ei kuvia tällä alueella", + "map_no_location_permission_content": "Paikannuslupa tarvitaan, jotta nykyisen sijainnin kohteita voidaan näyttää. Haluatko sallia pääsyn sijaintiin?", + "map_no_location_permission_title": "Paikannuslupa estetty", + "map_settings_dark_mode": "Tumma tila", + "map_settings_dialog_cancel": "Peruuta", + "map_settings_dialog_save": "Tallenna", + "map_settings_dialog_title": "Kartta-asetukset", + "map_settings_include_show_archived": "Sisällytä arkistoidut", + "map_settings_only_relative_range": "Päivämäärän rajaus", + "map_settings_only_show_favorites": "Näytä vain suosikit", + "map_zoom_to_see_photos": "Tarkenna nähdäksesi kuvat", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Liikekuvat", "notification_permission_dialog_cancel": "Peruuta", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} ei voi enää käyttää kuviasi.", "partner_page_stop_sharing_title": "Lopetetaanko kuvien jakaminen?", "partner_page_title": "Kumppani", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Jatka silti", "permission_onboarding_get_started": "Aloittaminen", "permission_onboarding_go_to_settings": "Siirry asetuksiin", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich vaatii käyttöoikeuden kuvien ja videoiden käyttämiseen.", "profile_drawer_app_logs": "Lokit", "profile_drawer_client_server_up_to_date": "Asiakassovellus ja palvelin ovat ajan tasalla", + "profile_drawer_documentation": "Dokumentaatio", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Asetukset", "profile_drawer_sign_out": "Kirjaudu ulos", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "Roskakori", "recently_added_page_title": "Viimeksi lisätyt", "search_bar_hint": "Etsi kuvia", "search_page_categories": "Kategoriat", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Albumin luonti epäonnistui", "select_user_for_sharing_page_share_suggestions": "Ehdotukset", "server_info_box_app_version": "Sovelluksen versio", + "server_info_box_server_url": "Palvelimen URL-osoite", "server_info_box_server_version": "Palvelimen versio", "setting_image_viewer_help": "Sovellus lataa ensin pienen esikatselukuvan, toisena keskitarkkuuksisen kuvan (jos käytössä) ja kolmantena alkuperäisen täysitarkkuuksisen kuvan (jos käytössä)", "setting_image_viewer_original_subtitle": "Ota käyttöön ladataksesi alkuperäinen täysitarkkuuksinen kuva (suuri!). Poista käytöstä vähentääksesi datan käyttöä (sekä verkossa että laitteen välimuistissa).", @@ -300,28 +315,37 @@ "share_add_photos": "Lisää kuvia", "share_add_title": "Lisää nimi", "share_create_album": "Luo albumi", + "shared_album_activities_input_disable": "Kommentointi on kytketty pois päältä", + "shared_album_activities_input_hint": "Sano jotain", + "shared_album_activity_remove_content": "Haluatko poistaa tämän aktiviteetin?", + "shared_album_activity_remove_title": "Poista aktiviteetti", + "shared_album_activity_setting_subtitle": "Anna muiden vastata", + "shared_album_activity_setting_title": "Kommentit ja tykkäykset", "share_dialog_preparing": "Valmistellaan...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_app_bar_title": "Jaetut linkit", + "shared_link_create_app_bar_title": "Luo linkki jaettavaksi", + "shared_link_create_info": "Salli kaikkien linkinhaltijoiden nähdä valitut kuvat", + "shared_link_create_submit_button": "Luo linkki", + "shared_link_edit_allow_download": "Salli julkisen käyttäjän ladata palvelimelta", + "shared_link_edit_allow_upload": "Salli julkisen käyttäjän siirtää palvelimelle", + "shared_link_edit_app_bar_title": "Muokkaa linkkiä", + "shared_link_edit_change_expiry": "Muuta erääntymisaikaa", + "shared_link_edit_description": "Kuvaus", + "shared_link_edit_description_hint": "Lisää jaon kuvaus", + "shared_link_edit_expire_after": "Umpeutuu", + "shared_link_edit_password": "Salasana", + "shared_link_edit_password_hint": "Syötä jaon salasana", + "shared_link_edit_show_meta": "Näytä metadata", + "shared_link_edit_submit_button": "Päivitä linkki", + "shared_link_empty": "Sinulla ei ole jaettuja linkkejä", + "shared_link_manage_links": "Hallitse jaettuja linkkejä", + "share_done": "Valmis", "share_invite": "Kutsu albumiin", "sharing_page_album": "Jaetut albumit", "sharing_page_description": "Luo jaettuja albumeja jakaaksesi kuvia ja videoita läheisillesi.", "sharing_page_empty_list": "TYHJÄ LISTA", "sharing_silver_appbar_create_shared_album": "Luo jaettu albumi", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "Jaetut linkit", "sharing_silver_appbar_share_partner": "Jaa kumppanille", "tab_controller_nav_library": "Kirjasto", "tab_controller_nav_photos": "Kuvat", @@ -337,19 +361,19 @@ "theme_setting_theme_title": "Teema", "theme_setting_three_stage_loading_subtitle": "Kolmivaiheinen lataaminen saattaa parantaa latauksen suorituskykyä, mutta lisää kaistankäyttöä huomattavasti.", "theme_setting_three_stage_loading_title": "Ota kolmivaiheinen lataus käyttöön", - "translated_text_options": "Options", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", + "translated_text_options": "Vaihtoehdot", + "trash_page_delete": "Poista", + "trash_page_delete_all": "Poista kaikki", + "trash_page_empty_trash_btn": "Tyhjennä roskakori", + "trash_page_empty_trash_dialog_content": "Haluatko poistaa roskakoriin siirretyt kohteet? Kohteet poistetaan lopullisesti Immich:sta.", "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", + "trash_page_info": "Roskakoriin siirretyt kohteet poistetaan lopullisesti {} päivän jälkeen", + "trash_page_no_assets": "Ei poistettuja kohteita", + "trash_page_restore": "Palauta", + "trash_page_restore_all": "Palauta kaikki", + "trash_page_select_assets_btn": "Valitse kohteet", + "trash_page_select_btn": "Valitse", + "trash_page_title": "Roskakori", "upload_dialog_cancel": "Peruuta", "upload_dialog_info": "Haluatko varmuuskopioida valitut kohteet palvelimelle?", "upload_dialog_ok": "Lähetä", @@ -360,7 +384,7 @@ "version_announcement_overlay_text_2": "Ota hetki aikaa vieraillaksesi", "version_announcement_overlay_text_3": "ja varmista, että käyttämäsi docker-compose ja .env-asetukset ovat ajantasalla välttyäksesi asetusongelmilta. Varsinkin jos käytät WatchToweria tai jotain muuta mekanismia päivittääksesi palvelinsovellusta automaattisesti.", "version_announcement_overlay_title": "Uusi palvelinversio saatavilla \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "Poista pinosta", + "viewer_stack_use_as_main_asset": "Käytä pääkohteena", + "viewer_unstack": "Pura pino" } \ No newline at end of file diff --git a/mobile/assets/i18n/fr-CA.json b/mobile/assets/i18n/fr-CA.json new file mode 100644 index 000000000..25e2615d9 --- /dev/null +++ b/mobile/assets/i18n/fr-CA.json @@ -0,0 +1,384 @@ +{ + "add_to_album_bottom_sheet_added": "Ajouté à {album}", + "add_to_album_bottom_sheet_already_exists": "Déjà dans {album}", + "advanced_settings_prefer_remote_subtitle": "Certains appareils sont très lents à charger des vignettes à partir de ressources présentes sur l'appareil. Activez ce paramètre pour charger des images externes à la place.", + "advanced_settings_prefer_remote_title": "Préférer les images externes", + "advanced_settings_self_signed_ssl_subtitle": "Permet d'ignorer la vérification du certificat SSL pour le point d'accès du serveur. Requis pour les certificats auto-signés.", + "advanced_settings_self_signed_ssl_title": "Autoriser les certificats SSL auto-signés", + "advanced_settings_tile_subtitle": "Paramètres d'utilisateur avancés", + "advanced_settings_tile_title": "Avancé", + "advanced_settings_troubleshooting_subtitle": "Activer des fonctions supplémentaires pour le dépannage", + "advanced_settings_troubleshooting_title": "Dépannage", + "album_info_card_backup_album_excluded": "EXCLUS", + "album_info_card_backup_album_included": "INCLUS", + "album_thumbnail_card_item": "1 élément", + "album_thumbnail_card_items": "{} éléments", + "album_thumbnail_card_shared": " · Partagé", + "album_thumbnail_owned": "Possédé", + "album_thumbnail_shared_by": "Partagé par {}", + "album_viewer_appbar_share_delete": "Supprimer l'album", + "album_viewer_appbar_share_err_delete": "Échec de la suppression de l'album", + "album_viewer_appbar_share_err_leave": "Impossible de quitter l'album", + "album_viewer_appbar_share_err_remove": "Il y a des problèmes lors de la suppression des éléments de l'album", + "album_viewer_appbar_share_err_title": "Échec de la modification du titre de l'album", + "album_viewer_appbar_share_leave": "Quitter l'album", + "album_viewer_appbar_share_remove": "Retirer de l'album", + "album_viewer_appbar_share_to": "Partager à", + "album_viewer_page_share_add_users": "Ajouter des utilisateurs", + "all_people_page_title": "Personnes", + "all_videos_page_title": "Vidéos", + "app_bar_signout_dialog_content": "Êtes-vous sûr de vouloir vous déconnecter?", + "app_bar_signout_dialog_ok": "Oui", + "app_bar_signout_dialog_title": "Se déconnecter", + "archive_page_no_archived_assets": "Aucun élément archivé n'a été trouvé", + "archive_page_title": "Archive ({})", + "asset_list_layout_settings_dynamic_layout_title": "Affichage dynamique", + "asset_list_layout_settings_group_automatically": "Automatique", + "asset_list_layout_settings_group_by": "Grouper les éléments par", + "asset_list_layout_settings_group_by_month": "Mois", + "asset_list_layout_settings_group_by_month_day": "Mois + jour", + "asset_list_settings_subtitle": "Paramètres de disposition de la grille de photos", + "asset_list_settings_title": "Grille de photos", + "backup_album_selection_page_albums_device": "Albums sur l'appareil ({})", + "backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure", + "backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.", + "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_all": "Tout", + "backup_background_service_backup_failed_message": "Échec de la sauvegarde des éléments. Nouvelle tentative...", + "backup_background_service_connection_failed_message": "Impossible de se connecter au serveur. Nouvelle tentative...", + "backup_background_service_current_upload_notification": "Transfert {}", + "backup_background_service_default_notification": "Recherche de nouveaux éléments...", + "backup_background_service_error_title": "Erreur de sauvegarde", + "backup_background_service_in_progress_notification": "Sauvegarde de vos éléments...", + "backup_background_service_upload_failure_notification": "Impossible de transférer {}", + "backup_controller_page_albums": "Sauvegarder les albums", + "backup_controller_page_background_app_refresh_disabled_content": "Activez le rafraîchissement de l'application en arrière-plan dans Paramètres > Général > Rafraîchissement de l'application en arrière-plan afin d'utiliser la sauvegarde en arrière-plan.", + "backup_controller_page_background_app_refresh_disabled_title": "Rafraîchissement de l'application en arrière-plan désactivé", + "backup_controller_page_background_app_refresh_enable_button_text": "Aller aux paramètres", + "backup_controller_page_background_battery_info_link": "Montrez-moi comment", + "backup_controller_page_background_battery_info_message": "Pour une expérience optimale de la sauvegarde en arrière-plan, veuillez désactiver toute optimisation de la batterie limitant l'activité en arrière-plan pour Immich.\n\nÉtant donné que cela est spécifique à chaque appareil, veuillez consulter les informations requises pour le fabricant de votre appareil.", + "backup_controller_page_background_battery_info_ok": "OK", + "backup_controller_page_background_battery_info_title": "Optimisation de la batterie", + "backup_controller_page_background_charging": "Seulement pendant la charge", + "backup_controller_page_background_configure_error": "Échec de la configuration du service d'arrière-plan", + "backup_controller_page_background_delay": "Retarder la sauvegarde des nouveaux éléments d'actif: {}", + "backup_controller_page_background_description": "Activez le service d'arrière-plan pour sauvegarder automatiquement tous les nouveaux éléments sans avoir à ouvrir l'application.", + "backup_controller_page_background_is_off": "La sauvegarde automatique en arrière-plan est désactivée", + "backup_controller_page_background_is_on": "La sauvegarde automatique en arrière-plan est activée", + "backup_controller_page_background_turn_off": "Désactiver le service d'arrière-plan", + "backup_controller_page_background_turn_on": "Activer le service d'arrière-plan", + "backup_controller_page_background_wifi": "Uniquement sur WiFi", + "backup_controller_page_backup": "Sauvegardé", + "backup_controller_page_backup_selected": "Sélectionné: ", + "backup_controller_page_backup_sub": "Photos et vidéos sauvegardées", + "backup_controller_page_cancel": "Annuler", + "backup_controller_page_created": "Créé le: {}", + "backup_controller_page_desc_backup": "Activez la sauvegarde pour envoyer automatiquement les nouveaux éléments sur le serveur.", + "backup_controller_page_excluded": "Exclus: ", + "backup_controller_page_failed": "Échec de l'opération ({})", + "backup_controller_page_filename": "Nom du fichier: {} [{}]", + "backup_controller_page_id": "ID: {}", + "backup_controller_page_info": "Informations de sauvegarde", + "backup_controller_page_none_selected": "Aucune sélection", + "backup_controller_page_remainder": "Restant", + "backup_controller_page_remainder_sub": "Photos et albums restants à sauvegarder à partir de la sélection", + "backup_controller_page_select": "Sélectionner", + "backup_controller_page_server_storage": "Stockage du serveur", + "backup_controller_page_start_backup": "Démarrer la sauvegarde", + "backup_controller_page_status_off": "La sauvegarde est désactivée", + "backup_controller_page_status_on": "La sauvegarde est activée", + "backup_controller_page_storage_format": "{} de {} utilisé", + "backup_controller_page_to_backup": "Albums à sauvegarder", + "backup_controller_page_total": "Total", + "backup_controller_page_total_sub": "Toutes les photos et vidéos uniques des albums sélectionnés", + "backup_controller_page_turn_off": "Désactiver la sauvegarde", + "backup_controller_page_turn_on": "Activer la sauvegarde", + "backup_controller_page_uploading_file_info": "Transfert des informations du fichier", + "backup_err_only_album": "Impossible de retirer le seul album", + "backup_info_card_assets": "éléments", + "backup_manual_cancelled": "Annulé", + "backup_manual_failed": "Echec", + "backup_manual_in_progress": "Téléchargement déjà en cours. Essayez après un instant", + "backup_manual_success": "Succès ", + "backup_manual_title": "Statut du téléchargement ", + "cache_settings_album_thumbnails": "vignettes de la page bibliothèque ({} éléments)", + "cache_settings_clear_cache_button": "Effacer le cache", + "cache_settings_clear_cache_button_title": "Efface le cache de l'application. Cela aura un impact significatif sur les performances de l'application jusqu'à ce que le cache soit reconstruit.", + "cache_settings_image_cache_size": "Taille du cache des images ({} éléments)", + "cache_settings_statistics_album": "vignettes de la bibliothèque", + "cache_settings_statistics_assets": "{} éléments ({})", + "cache_settings_statistics_full": "Images complètes", + "cache_settings_statistics_shared": "vignettes d'albums partagés", + "cache_settings_statistics_thumbnail": "vignettes", + "cache_settings_statistics_title": "Utilisation du cache", + "cache_settings_subtitle": "Contrôler le comportement de mise en cache de l'application mobile Immich", + "cache_settings_thumbnail_size": "Taille du cache des vignettes ({} éléments)", + "cache_settings_tile_subtitle": "Contrôler le comportement du stockage local", + "cache_settings_tile_title": "Stockage local", + "cache_settings_title": "Paramètres de mise en cache", + "change_password_form_confirm_password": "Confirmez le mot de passe", + "change_password_form_description": "Bonjour {name},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé de changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.", + "change_password_form_new_password": "Nouveau mot de passe", + "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", + "common_add_to_album": "Ajouter à l'album", + "common_change_password": "Modifier le mot de passe", + "common_create_new_album": "Créer un nouvel album", + "common_server_error": "Veuillez vérifier votre connexion réseau, vous assurer que le serveur est accessible et que les versions de l'application et du serveur sont compatibles.", + "common_shared": "Partagé", + "control_bottom_app_bar_add_to_album": "Ajouter à l'album", + "control_bottom_app_bar_album_info": "{} éléments", + "control_bottom_app_bar_album_info_shared": "{} éléments - Partagés", + "control_bottom_app_bar_archive": "Archive", + "control_bottom_app_bar_create_new_album": "Créer un nouvel album", + "control_bottom_app_bar_delete": "Supprimer", + "control_bottom_app_bar_favorite": "Favoris", + "control_bottom_app_bar_share": "Partager", + "control_bottom_app_bar_share_to": "Partager à", + "control_bottom_app_bar_stack": "Empiler", + "control_bottom_app_bar_unarchive": "Désarchiver", + "control_bottom_app_bar_upload": "Téléverser", + "create_album_page_untitled": "Sans titre", + "create_shared_album_page_create": "Créer", + "create_shared_album_page_share": "Partager", + "create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS", + "create_shared_album_page_share_select_photos": "Sélectionner les photos", + "curated_location_page_title": "Places", + "curated_object_page_title": "Objets", + "daily_title_text_date": "E, dd MMM", + "daily_title_text_date_year": "E, dd MMM, yyyy", + "date_format": "E, LLL d, y • h:mm a", + "delete_dialog_alert": "Ces éléments seront définitivement supprimés de Immich et de votre appareil.", + "delete_dialog_cancel": "Annuler", + "delete_dialog_ok": "Supprimer", + "delete_dialog_title": "Supprimer définitivement", + "delete_shared_link_dialog_content": "Êtes-vous sûr de vouloir supprimer ce lien partagé?", + "delete_shared_link_dialog_title": "Supprimer le lien partagé", + "description_input_hint_text": "Ajouter une description...", + "description_input_submit_error": "Erreur de mise à jour de la description, vérifier le journal pour plus de détails", + "exif_bottom_sheet_description": "Ajouter une description...", + "exif_bottom_sheet_details": "DÉTAILS", + "exif_bottom_sheet_location": "LOCALISATION", + "experimental_settings_new_asset_list_subtitle": "En cours de développement", + "experimental_settings_new_asset_list_title": "Activer la grille de photos expérimentale", + "experimental_settings_subtitle": "Utilisez à vos dépends!", + "experimental_settings_title": "Expérimental", + "favorites_page_no_favorites": "Aucun élément favori n'a été trouvé", + "favorites_page_title": "Favoris", + "home_page_add_to_album_conflicts": "{added} éléments ajoutés à l'album {album}. Les éléments {failed} sont déjà dans l'album.", + "home_page_add_to_album_err_local": "Impossible d'ajouter des éléments locaux aux albums pour le moment, étape ignorée", + "home_page_add_to_album_success": "{added} éléments ajoutés à l'album {album}.", + "home_page_archive_err_local": "Impossible d'archiver les ressources locales pour l'instant, étape ignorée", + "home_page_building_timeline": "Construction de la chronologie", + "home_page_favorite_err_local": "Impossible d'ajouter des éléments locaux aux favoris pour le moment, étape ignorée", + "home_page_first_time_notice": "Si c'est la première fois que vous utilisez l'application, veillez à choisir un ou plusieurs albums de sauvegarde afin que la chronologie puisse alimenter les photos et les vidéos de cet ou ces albums.", + "home_page_upload_err_limit": "Limite de téléchargement de 30 éléments en même temps, demande ignorée", + "image_viewer_page_state_provider_download_error": "Erreur de téléchargement", + "image_viewer_page_state_provider_download_success": "Téléchargement réussi", + "image_viewer_page_state_provider_share_error": "Erreur de partage", + "library_page_albums": "Albums", + "library_page_archive": "Archive", + "library_page_device_albums": "Albums sur l'appareil", + "library_page_favorites": "Favoris", + "library_page_new_album": "Nouvel album", + "library_page_sharing": "Partage", + "library_page_sort_created": "Créations les plus récentes", + "library_page_sort_last_modified": "Dernière modification", + "library_page_sort_most_recent_photo": "Photo la plus récente", + "library_page_sort_title": "Titre de l'album", + "login_disabled": "La connexion a été désactivée ", + "login_form_api_exception": "Erreur de l'API. Veuillez vérifier l'URL du serveur et et réessayer.", + "login_form_button_text": "Connexion", + "login_form_email_hint": "votreemail@email.com", + "login_form_endpoint_hint": "http://adresse-ip-serveur:port/api", + "login_form_endpoint_url": "URL du point d'accès au serveur", + "login_form_err_http": "Veuillez préciser http:// ou https://", + "login_form_err_invalid_email": "E-mail invalide", + "login_form_err_invalid_url": "URL invalide", + "login_form_err_leading_whitespace": "Espace en début de ligne", + "login_form_err_trailing_whitespace": "Espace de fin de ligne", + "login_form_failed_get_oauth_server_config": "Erreur de connexion par OAuth, vérifiez l\"URL du serveur", + "login_form_failed_get_oauth_server_disable": "La fonctionnalité OAuth n'est pas disponible sur ce serveur", + "login_form_failed_login": "Erreur de connexion, vérifiez l'url du serveur, l'email et le mot de passe", + "login_form_handshake_exception": "Il y a eu une exception de liaison avec le serveur. Activez la prise en charge des certificats auto-signés dans les paramètres si vous utilisez un certificat auto-signé.", + "login_form_label_email": "E-mail", + "login_form_label_password": "Mot de passe", + "login_form_next_button": "Suivant", + "login_form_password_hint": "mot de passe", + "login_form_save_login": "Rester connecté", + "login_form_server_empty": "Saisissez l'URL du serveur.", + "login_form_server_error": "Impossible de se connecter au serveur.", + "login_password_changed_error": "Une erreur s'est produite lors de la mise à jour de votre mot de passe", + "login_password_changed_success": "Mot de passe mis à jour avec succès", + "map_cannot_get_user_location": "Impossible d'obtenir la localisation de l'utilisateur", + "map_location_dialog_cancel": "Annuler", + "map_location_dialog_yes": "Oui", + "map_location_service_disabled_content": "Le service de localisation doit être activé pour afficher les éléments de votre emplacement actuel. Souhaitez-vous l'activer maintenant?", + "map_location_service_disabled_title": "Service de localisation désactivé", + "map_no_assets_in_bounds": "Pas de photos dans cette zone", + "map_no_location_permission_content": "L'autorisation de localisation est nécessaire pour afficher les éléments de votre emplacement actuel. Souhaitez-vous l'autoriser maintenant?", + "map_no_location_permission_title": "Permission de localisation refusée", + "map_settings_dark_mode": "Mode sombre", + "map_settings_dialog_cancel": "Annuler", + "map_settings_dialog_save": "Sauvegarder", + "map_settings_dialog_title": "Paramètres de la carte", + "map_settings_include_show_archived": "Inclure les archives", + "map_settings_only_relative_range": "Plage de dates", + "map_settings_only_show_favorites": "Afficher uniquement les favoris", + "map_zoom_to_see_photos": "Dézoomer pour voir les photos", + "monthly_title_text_date_format": "MMMM y", + "motion_photos_page_title": "Photos avec mouvement", + "notification_permission_dialog_cancel": "Annuler", + "notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.", + "notification_permission_dialog_settings": "Paramètres", + "notification_permission_list_tile_content": "Accordez la permission d'activer les notifications.", + "notification_permission_list_tile_enable_button": "Activer les notifications", + "notification_permission_list_tile_title": "Permission de notification", + "partner_page_add_partner": "Ajouter un partenaire", + "partner_page_empty_message": "Vos photos ne sont pas encore partagées avec un partenaire.", + "partner_page_no_more_users": "Plus d'utilisateurs à ajouter", + "partner_page_partner_add_failed": "Échec de l'ajout d'un partenaire", + "partner_page_select_partner": "Sélectionner un partenaire", + "partner_page_shared_to_title": "Partagé avec", + "partner_page_stop_sharing_content": "{} ne pourra plus accéder à vos photos.", + "partner_page_stop_sharing_title": "Arrêter de partager vos photos?", + "partner_page_title": "Partenaire", + "permission_onboarding_continue_anyway": "Continuer quand même", + "permission_onboarding_get_started": "Commencer", + "permission_onboarding_go_to_settings": "Accéder aux paramètres", + "permission_onboarding_grant_permission": "Accorder l'autorisation", + "permission_onboarding_log_out": "Se déconnecter", + "permission_onboarding_permission_denied": "Permission refusée. Pour utiliser Immich, accordez lautorisation pour les photos et vidéos dans les Paramètres.", + "permission_onboarding_permission_granted": "Permission accordée! Vous êtes prêts.", + "permission_onboarding_permission_limited": "Permission limitée. Pour permettre à Immich de sauvegarder et de gérer l'ensemble de votre bibliothèque, accordez l'autorisation pour les photos et vidéos dans les Paramètres.", + "permission_onboarding_request": "Immich demande l'autorisation de visionner vos photos et vidéo", + "profile_drawer_app_logs": "Journaux", + "profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", + "profile_drawer_settings": "Paramètres", + "profile_drawer_sign_out": "Se déconnecter", + "profile_drawer_trash": "Corbeille", + "recently_added_page_title": "Récemment ajouté", + "search_bar_hint": "Rechercher vos photos", + "search_page_categories": "Catégories", + "search_page_favorites": "Favoris", + "search_page_motion_photos": "Photos avec mouvement", + "search_page_no_objects": "Aucune information disponible sur les objets", + "search_page_no_places": "Aucune information disponible sur la localisation", + "search_page_people": "Personnes", + "search_page_places": "Lieux", + "search_page_recently_added": "Récemment ajouté", + "search_page_screenshots": "Captures d'écran", + "search_page_selfies": "Selfies", + "search_page_things": "Objets", + "search_page_videos": "Vidéos", + "search_page_view_all_button": "Voir tout", + "search_page_your_activity": "Votre activité", + "search_result_page_new_search_hint": "Nouvelle recherche", + "search_suggestion_list_smart_search_hint_1": "La recherche intelligente est activée par défaut. Pour rechercher des métadonnées, utilisez la syntaxe suivante", + "search_suggestion_list_smart_search_hint_2": "m:votre-terme-de-recherche", + "select_additional_user_for_sharing_page_suggestions": "Suggestions", + "select_user_for_sharing_page_err_album": "Échec de la création de l'album", + "select_user_for_sharing_page_share_suggestions": "Suggestions", + "server_info_box_app_version": "Version de l'application", + "server_info_box_server_url": "URL du serveur", + "server_info_box_server_version": "Version du serveur", + "setting_image_viewer_help": "Le visualiseur de détails charge d'abord la petite vignette, puis l'aperçu de taille moyenne (s'il est activé), enfin l'original (s'il est activé).", + "setting_image_viewer_original_subtitle": "Activez cette option pour charger l'image en résolution originale (volumineux!). Désactiver pour réduire l'utilisation des données (réseau et cache de l'appareil).", + "setting_image_viewer_original_title": "Charger l'image originale", + "setting_image_viewer_preview_subtitle": "Activer pour charger une image de résolution moyenne. Désactiver pour charger directement l'original ou utiliser uniquement la vignette.", + "setting_image_viewer_preview_title": "Charger l'image d'aperçu", + "setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan: {}", + "setting_notifications_notify_hours": "{} heures", + "setting_notifications_notify_immediately": "immédiatement", + "setting_notifications_notify_minutes": "{} minutes", + "setting_notifications_notify_never": "jamais", + "setting_notifications_notify_seconds": "{} secondes", + "setting_notifications_single_progress_subtitle": "Informations détaillées sur la progression du transfert par élément", + "setting_notifications_single_progress_title": "Afficher la progression du détail de la sauvegarde en arrière-plan", + "setting_notifications_subtitle": "Ajustez vos préférences de notification", + "setting_notifications_title": "Notifications", + "setting_notifications_total_progress_subtitle": "Progression globale du transfert (effectué/total des éléments)", + "setting_notifications_total_progress_title": "Afficher la progression totale de la sauvegarde en arrière-plan", + "setting_pages_app_bar_settings": "Paramètres", + "settings_require_restart": "Veuillez redémarrer Immich pour appliquer ce paramètre", + "share_add": "Ajouter", + "share_add_photos": "Ajouter des photos", + "share_add_title": "Ajouter un titre", + "share_create_album": "Créer un album", + "shared_album_activities_input_disable": "Les commentaires sont désactivés", + "shared_album_activities_input_hint": "Dire quelque chose", + "shared_album_activity_remove_content": "Souhaitez-vous supprimer cette activité?", + "shared_album_activity_remove_title": "Supprimer l'activité", + "shared_album_activity_setting_subtitle": "Laisser les autres réagir", + "shared_album_activity_setting_title": "Commentaires et likes", + "share_dialog_preparing": "Préparation...", + "shared_link_app_bar_title": "Liens partagés", + "shared_link_create_app_bar_title": "Créer un lien pour partager", + "shared_link_create_info": "Permettre à toute personne ayant le lien de voir la ou les photos sélectionnées", + "shared_link_create_submit_button": "Créer le lien", + "shared_link_edit_allow_download": "Autoriser les utilisateurs publics à télécharger", + "shared_link_edit_allow_upload": "Autoriser les utilisateurs publics à téléverser", + "shared_link_edit_app_bar_title": "Modifier le lien", + "shared_link_edit_change_expiry": "Modifier le délai d'expiration", + "shared_link_edit_description": "Description", + "shared_link_edit_description_hint": "Saisir la description du partage", + "shared_link_edit_expire_after": "Expire après", + "shared_link_edit_password": "Mot de passe", + "shared_link_edit_password_hint": "Saisir le mot de passe de partage", + "shared_link_edit_show_meta": "Afficher les métadonnées", + "shared_link_edit_submit_button": "Mettre à jour le lien", + "shared_link_empty": "Vous n'avez pas de liens partagés", + "shared_link_manage_links": "Gérer les liens partagés", + "share_done": "Fait", + "share_invite": "Inviter à l'album", + "sharing_page_album": "Albums partagés", + "sharing_page_description": "Créez des albums partagés pour partager des photos et des vidéos avec les personnes de votre réseau.", + "sharing_page_empty_list": "LISTE VIDE", + "sharing_silver_appbar_create_shared_album": "Créer un album partagé", + "sharing_silver_appbar_shared_links": "Liens partagés", + "sharing_silver_appbar_share_partner": "Partager avec un partenaire", + "tab_controller_nav_library": "Bibliothèque", + "tab_controller_nav_photos": "Photos", + "tab_controller_nav_search": "Recherche", + "tab_controller_nav_sharing": "Partage", + "theme_setting_asset_list_storage_indicator_title": "Afficher l'indicateur de stockage sur les tuiles des éléments", + "theme_setting_asset_list_tiles_per_row_title": "Nombre d'éléments par ligne ({})", + "theme_setting_dark_mode_switch": "Mode sombre", + "theme_setting_image_viewer_quality_subtitle": "Ajustez la qualité de la visionneuse d'images détaillées", + "theme_setting_image_viewer_quality_title": "Qualité de la visualisation des images", + "theme_setting_system_theme_switch": "Automatique (suivre les paramètres du système)", + "theme_setting_theme_subtitle": "Choisissez le thème de l'application", + "theme_setting_theme_title": "Thème", + "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", + "translated_text_options": "Options", + "trash_page_delete": "Supprimer", + "trash_page_delete_all": "Tout supprimer", + "trash_page_empty_trash_btn": "Vider la corbeille", + "trash_page_empty_trash_dialog_content": "Voulez-vous vider les éléments de la corbeille? Ces objets seront définitivement retirés d'Immich", + "trash_page_empty_trash_dialog_ok": "Ok", + "trash_page_info": "Les éléments mis à la corbeille seront définitivement supprimés au bout de {} jours.", + "trash_page_no_assets": "Pas d'éléments dans la corbeille", + "trash_page_restore": "Restaurer", + "trash_page_restore_all": "Tout restaurer", + "trash_page_select_assets_btn": "Sélectionner les éléments", + "trash_page_select_btn": "Sélectionner", + "trash_page_title": "Corbeille ({})", + "upload_dialog_cancel": "Annuler", + "upload_dialog_info": "Voulez-vous sauvegarder la sélection vers le serveur?", + "upload_dialog_ok": "Télécharger ", + "upload_dialog_title": "Télécharger cet élément ", + "version_announcement_overlay_ack": "Confirmer", + "version_announcement_overlay_release_notes": "notes de mise à jour", + "version_announcement_overlay_text_1": "Bonjour, une nouvelle version de", + "version_announcement_overlay_text_2": "veuillez prendre le temps de visiter le ", + "version_announcement_overlay_text_3": " et assurez-vous que votre configuration docker-compose et .env est à jour pour éviter toute erreur de configuration, en particulier si vous utilisez WatchTower ou tout autre mécanisme qui gère la mise à jour automatique de votre application serveur.", + "version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89", + "viewer_remove_from_stack": "Retirer de la pile", + "viewer_stack_use_as_main_asset": "Utiliser comme élément principal", + "viewer_unstack": "Désempiler" + } \ No newline at end of file diff --git a/mobile/assets/i18n/fr-FR.json b/mobile/assets/i18n/fr-FR.json index 5eacd5b56..22050072d 100644 --- a/mobile/assets/i18n/fr-FR.json +++ b/mobile/assets/i18n/fr-FR.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Échec de la modification du titre de l'album", "album_viewer_appbar_share_leave": "Quitter l'album", "album_viewer_appbar_share_remove": "Retirer de l'album", + "album_viewer_appbar_share_to": "Partager à", "album_viewer_page_share_add_users": "Ajouter des utilisateurs", "all_people_page_title": "Personnes", "all_videos_page_title": "Vidéos", + "app_bar_signout_dialog_content": "Êtes-vous sûr de vouloir vous déconnecter ?", + "app_bar_signout_dialog_ok": "Oui", + "app_bar_signout_dialog_title": "Se déconnecter", "archive_page_no_archived_assets": "Aucun élément archivé n'a été trouvé", "archive_page_title": "Archive ({})", "asset_list_layout_settings_dynamic_layout_title": "Affichage dynamique", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Utilisation du cache", "cache_settings_subtitle": "Contrôler le comportement de mise en cache de l'application mobile Immich", "cache_settings_thumbnail_size": "Taille du cache des miniatures ({} éléments)", + "cache_settings_tile_subtitle": "Contrôler le comportement du stockage local", + "cache_settings_tile_title": "Stockage local", "cache_settings_title": "Paramètres de mise en cache", "change_password_form_confirm_password": "Confirmez le mot de passe", - "change_password_form_description": "Bonjour {firstName} {lastName},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé à changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.", + "change_password_form_description": "Bonjour {name},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé à changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.", "change_password_form_new_password": "Nouveau mot de passe", "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", @@ -130,10 +136,10 @@ "control_bottom_app_bar_delete": "Supprimer", "control_bottom_app_bar_favorite": "Favoris", "control_bottom_app_bar_share": "Partager", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_share_to": "Partager à", + "control_bottom_app_bar_stack": "Empiler", "control_bottom_app_bar_unarchive": "Désarchiver", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "Téléverser", "create_album_page_untitled": "Sans titre", "create_shared_album_page_create": "Créer", "create_shared_album_page_share": "Partager", @@ -148,8 +154,8 @@ "delete_dialog_cancel": "Annuler", "delete_dialog_ok": "Supprimer", "delete_dialog_title": "Supprimer définitivement", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_content": "Êtes-vous sûr de vouloir supprimer ce lien partagé ?", + "delete_shared_link_dialog_title": "Supprimer le lien partagé", "description_input_hint_text": "Ajouter une description...", "description_input_submit_error": "Erreur de mise à jour de la description, vérifier le journal pour plus de détails", "exif_bottom_sheet_description": "Ajouter une description...", @@ -164,14 +170,19 @@ "home_page_add_to_album_conflicts": "{added} éléments ajoutés à l'album {album}. Les éléments {failed} sont déjà dans l'album.", "home_page_add_to_album_err_local": "Impossible d'ajouter des éléments locaux aux albums pour le moment, étape ignorée", "home_page_add_to_album_success": "{added} éléments ajoutés à l'album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Impossible d'archiver les ressources locales pour l'instant, étape ignorée", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Construction de la chronologie", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Impossible d'ajouter des éléments locaux aux favoris pour le moment, étape ignorée", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Si c'est la première fois que vous utilisez l'application, veillez à choisir un ou plusieurs albums de sauvegarde afin que la chronologie puisse alimenter les photos et les vidéos de cet ou ces albums.", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Limite de téléchargement de 30 éléments en même temps, demande ignorée", "image_viewer_page_state_provider_download_error": "Erreur de téléchargement", "image_viewer_page_state_provider_download_success": "Téléchargement réussi", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "Erreur de partage", "library_page_albums": "Albums", "library_page_archive": "Archive", "library_page_device_albums": "Albums sur l'appareil", @@ -179,8 +190,8 @@ "library_page_new_album": "Nouvel album", "library_page_sharing": "Partage", "library_page_sort_created": "Créations les plus récentes", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "Dernière modification", + "library_page_sort_most_recent_photo": "Photo la plus récente", "library_page_sort_title": "Titre de l'album", "login_disabled": "La connexion a été désactivée ", "login_form_api_exception": "Erreur de l'API. Veuillez vérifier l'URL du serveur et et réessayer.", @@ -218,7 +229,7 @@ "map_settings_dialog_cancel": "Annuler", "map_settings_dialog_save": "Sauvegarder", "map_settings_dialog_title": "Paramètres de la carte", - "map_settings_include_show_archived": "Include Archived", + "map_settings_include_show_archived": "Inclure les archives", "map_settings_only_relative_range": "Plage de dates", "map_settings_only_show_favorites": "Afficher uniquement les favoris", "map_zoom_to_see_photos": "Dézoomer pour voir les photos", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} ne pourra plus accéder à vos photos.", "partner_page_stop_sharing_title": "Arrêter de partager vos photos ?", "partner_page_title": "Partenaire", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continuer quand même", "permission_onboarding_get_started": "Commencer", "permission_onboarding_go_to_settings": "Accéder aux paramètres", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich demande l'autorisation de visionner vos photos et vidéo", "profile_drawer_app_logs": "Journaux", "profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Paramètres", "profile_drawer_sign_out": "Se déconnecter", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "Corbeille", "recently_added_page_title": "Récemment ajouté", "search_bar_hint": "Rechercher vos photos", "search_page_categories": "Catégories", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Échec de la création de l'album", "select_user_for_sharing_page_share_suggestions": "Suggestions", "server_info_box_app_version": "Version de l'application", + "server_info_box_server_url": "URL du serveur", "server_info_box_server_version": "Version du serveur", "setting_image_viewer_help": "Le visualiseur de détails charge d'abord la petite miniature, puis l'aperçu de taille moyenne (s'il est activé), enfin l'original (s'il est activé).", "setting_image_viewer_original_subtitle": "Activez cette option pour charger l'image en résolution originale (volumineux !). Désactiver pour réduire l'utilisation des données (réseau et cache de l'appareil).", @@ -300,28 +315,37 @@ "share_add_photos": "Ajouter des photos", "share_add_title": "Ajouter un titre", "share_create_album": "Créer un album", + "shared_album_activities_input_disable": "Les commentaires sont désactivés", + "shared_album_activities_input_hint": "Dire quelque chose", + "shared_album_activity_remove_content": "Souhaitez-vous supprimer cette activité ?", + "shared_album_activity_remove_title": "Supprimer l'activité", + "shared_album_activity_setting_subtitle": "Laisser les autres réagir", + "shared_album_activity_setting_title": "Commentaires et likes", "share_dialog_preparing": "Préparation...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", + "shared_link_app_bar_title": "Liens partagés", + "shared_link_create_app_bar_title": "Créer un lien pour partager", + "shared_link_create_info": "Permettre à toute personne ayant le lien de voir la ou les photos sélectionnées", + "shared_link_create_submit_button": "Créer le lien", + "shared_link_edit_allow_download": "Autoriser les utilisateurs publics à télécharger", + "shared_link_edit_allow_upload": "Autoriser les utilisateurs publics à téléverser", + "shared_link_edit_app_bar_title": "Modifier le lien", + "shared_link_edit_change_expiry": "Modifier le délai d'expiration", "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_edit_description_hint": "Saisir la description du partage", + "shared_link_edit_expire_after": "Expire après", + "shared_link_edit_password": "Mot de passe", + "shared_link_edit_password_hint": "Saisir le mot de passe de partage", + "shared_link_edit_show_meta": "Afficher les métadonnées", + "shared_link_edit_submit_button": "Mettre à jour le lien", + "shared_link_empty": "Vous n'avez pas de liens partagés", + "shared_link_manage_links": "Gérer les liens partagés", + "share_done": "Fait", "share_invite": "Inviter à l'album", "sharing_page_album": "Albums partagés", "sharing_page_description": "Créez des albums partagés pour partager des photos et des vidéos avec les personnes de votre réseau.", "sharing_page_empty_list": "LISTE VIDE", "sharing_silver_appbar_create_shared_album": "Créer un album partagé", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "Liens partagés", "sharing_silver_appbar_share_partner": "Partager avec un partenaire", "tab_controller_nav_library": "Bibliothèque", "tab_controller_nav_photos": "Photos", @@ -338,18 +362,18 @@ "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", "translated_text_options": "Options", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", + "trash_page_delete": "Supprimer", + "trash_page_delete_all": "Tout supprimer", + "trash_page_empty_trash_btn": "Vider la corbeille", + "trash_page_empty_trash_dialog_content": "Voulez-vous vider les éléments de la corbeille? Ces objets seront définitivement retirés d'Immich", "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", + "trash_page_info": "Les éléments mis à la corbeille seront définitivement supprimés au bout de {} jours.", + "trash_page_no_assets": "Pas d'éléments dans la corbeille", + "trash_page_restore": "Restaurer", + "trash_page_restore_all": "Tout restaurer", + "trash_page_select_assets_btn": "Sélectionner les éléments", + "trash_page_select_btn": "Sélectionner", + "trash_page_title": "Corbeille ({})", "upload_dialog_cancel": "Annuler", "upload_dialog_info": "Voulez-vous sauvegarder la sélection vers le serveur ?", "upload_dialog_ok": "Télécharger ", @@ -360,7 +384,7 @@ "version_announcement_overlay_text_2": "veuillez prendre le temps de visiter le ", "version_announcement_overlay_text_3": " et assurez-vous que votre configuration docker-compose et .env est à jour pour éviter toute erreur de configuration, en particulier si vous utilisez WatchTower ou tout autre mécanisme qui gère la mise à jour automatique de votre application serveur.", "version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "Retirer de la pile", + "viewer_stack_use_as_main_asset": "Utiliser comme élément principal", + "viewer_unstack": "Désempiler" } \ No newline at end of file diff --git a/mobile/assets/i18n/hi-IN.json b/mobile/assets/i18n/hi-IN.json index 31311535b..9c42afb7d 100644 --- a/mobile/assets/i18n/hi-IN.json +++ b/mobile/assets/i18n/hi-IN.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Failed to change album title", "album_viewer_appbar_share_leave": "Leave album", "album_viewer_appbar_share_remove": "Remove from album", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Add users", "all_people_page_title": "People", "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", "archive_page_title": "Archive ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Cache usage", "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", @@ -164,10 +170,15 @@ "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", "home_page_add_to_album_success": "Added {added} assets to album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Can not archive local assets yet, skipping", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Building the timeline", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "image_viewer_page_state_provider_download_error": "Download Error", "image_viewer_page_state_provider_download_success": "Download Success", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Get started", "permission_onboarding_go_to_settings": "Go to settings", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "profile_drawer_app_logs": "Logs", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Settings", "profile_drawer_sign_out": "Sign Out", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Failed to create album", "select_user_for_sharing_page_share_suggestions": "Suggestions", "server_info_box_app_version": "App Version", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "Server Version", "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", @@ -300,6 +315,12 @@ "share_add_photos": "Add photos", "share_add_title": "Add a title", "share_create_album": "Create album", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Preparing...", "shared_link_app_bar_title": "Shared Links", "shared_link_create_app_bar_title": "Create link to share", @@ -311,6 +332,9 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", diff --git a/mobile/assets/i18n/hu-HU.json b/mobile/assets/i18n/hu-HU.json index cc177bffb..60bf00d99 100644 --- a/mobile/assets/i18n/hu-HU.json +++ b/mobile/assets/i18n/hu-HU.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Hiba az album átnevezése közben", "album_viewer_appbar_share_leave": "Kilépés az albumból", "album_viewer_appbar_share_remove": "Törlés az albumból", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Felhasználók hozzáadása", "all_people_page_title": "Emberek", "all_videos_page_title": "Videók", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "Nem található archivált média", "archive_page_title": "Archívum ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", @@ -111,6 +115,8 @@ "cache_settings_statistics_title": "Gyorsítótár által használt terület", "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Gyorsítótár beállítások", "change_password_form_confirm_password": "Jelszó Megerősítése", "change_password_form_description": "Kedves {lastName} {firstName}!\n\nMost jelentkezel be először a rendszerbe vagy más okból szükséfes a jelszavad meváltoztatása. Kérjük, add meg új jelszavad.", @@ -164,10 +170,15 @@ "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", "home_page_add_to_album_err_local": "Helyi médiát még nem lehet albumba tenni. Kihagyjuk.", "home_page_add_to_album_success": "Added {added} assets to album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Helyi média archiválása még nem támogatott, úgyhogy kihagyjuk", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Building the timeline", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Helyi médiát még nem lehet a kedvencek közé tenni. Kihagyjuk.", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Csak 30 elemet tudsz egyszerre feltölteni, átugrás", "image_viewer_page_state_provider_download_error": "Letöltési Hiba", "image_viewer_page_state_provider_download_success": "Letöltés Sikeres", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Folytatás mindenképp", "permission_onboarding_get_started": "Kezdjük el", "permission_onboarding_go_to_settings": "Beállítások megnyitása", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Engedélyezni kell, hogy az Immich hozzáférjen a képekhez és videókhoz", "profile_drawer_app_logs": "Naplók", "profile_drawer_client_server_up_to_date": "Kliens és a szerver is naprakész", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Beállítások", "profile_drawer_sign_out": "Kijelentkezés", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Hiba az album létrehozása közben", "select_user_for_sharing_page_share_suggestions": "Javaslatok", "server_info_box_app_version": "Alkalmazás Verzió", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "Szerver Verzió", "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", @@ -300,6 +315,12 @@ "share_add_photos": "Fotók hozzáadása", "share_add_title": "Cím hozzáadása", "share_create_album": "Album létrehozása", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Előkészítés...", "shared_link_app_bar_title": "Shared Links", "shared_link_create_app_bar_title": "Create link to share", @@ -311,6 +332,9 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", diff --git a/mobile/assets/i18n/it-IT.json b/mobile/assets/i18n/it-IT.json index 479a46016..d8a372527 100644 --- a/mobile/assets/i18n/it-IT.json +++ b/mobile/assets/i18n/it-IT.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Impossibile cambiare il titolo dell'album ", "album_viewer_appbar_share_leave": "Lascia album", "album_viewer_appbar_share_remove": "Rimuovere dall'album ", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Aggiungi utenti", "all_people_page_title": "Persone", "all_videos_page_title": "Video", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "Nessuna oggetto archiviato", "archive_page_title": "Archivia ({})", "asset_list_layout_settings_dynamic_layout_title": "Layout dinamico", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Uso della cache", "cache_settings_subtitle": "Controlla il comportamento della cache dell'applicazione mobile immich", "cache_settings_thumbnail_size": "Dimensione cache dei thumbnail ({} assets)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Impostazioni della Cache", "change_password_form_confirm_password": "Conferma Password ", - "change_password_form_description": "Ciao {firstName} {lastName},\n\nQuesto è la prima volta che accedi al sistema oppure è stato fatto una richiesta di cambiare la password. Per favore inserisca la nuova password qui sotto", + "change_password_form_description": "Ciao {name},\n\nQuesto è la prima volta che accedi al sistema oppure è stato fatto una richiesta di cambiare la password. Per favore inserisca la nuova password qui sotto", "change_password_form_new_password": "Nuova Password", "change_password_form_password_mismatch": "Le password non coincidono", "change_password_form_reenter_new_password": "Inserisci ancora la nuova password ", @@ -164,10 +170,15 @@ "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 negli album foto ancora non caricate", "home_page_add_to_album_success": "Aggiunti {added} elementi all'album {album}", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Non puoi archiviare immagini non ancora caricate", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Costruendo il Timeline", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Non puoi aggiungere tra i preferiti le foto ancora non caricate", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Se è la prima volta che usi l'app, assicurati di scegliere gli album per avere il Timeline con immagini e video", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Puoi caricare al massimo 30 file per volta, ignora quelli in eccesso", "image_viewer_page_state_provider_download_error": "Errore nel Download", "image_viewer_page_state_provider_download_success": "Download con successo", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} non sarà più in grado di accedere alle tue foto.", "partner_page_stop_sharing_title": "Stoppare la condivisione delle tue foto?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continua lo stesso", "permission_onboarding_get_started": "Inizia", "permission_onboarding_go_to_settings": "Vai a Impostazioni", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Immich richiede i permessi per vedere le tue foto e video", "profile_drawer_app_logs": "Logs", "profile_drawer_client_server_up_to_date": "Client e server sono aggiornati", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Impostazioni ", "profile_drawer_sign_out": "Logout", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Impossibile nel creare l'album ", "select_user_for_sharing_page_share_suggestions": "Suggerimenti", "server_info_box_app_version": "Versione App", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "Versione Server", "setting_image_viewer_help": "Il visualizzatore dettagliato carica una piccola thumbnail per prima, per poi caricare un immagine di media grandezza (se abilitato). Ed infine carica l'originale (se abilitato).", "setting_image_viewer_original_subtitle": "Abilita per caricare l'immagine originale a risoluzione massima (grande!). Disabilita per ridurre l'utilizzo di banda (sia sul network che nella cache del dispositivo).", @@ -300,6 +315,12 @@ "share_add_photos": "Aggiungi foto", "share_add_title": "Aggiungi un titolo ", "share_create_album": "Crea album", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Preparo…", "shared_link_app_bar_title": "Shared Links", "shared_link_create_app_bar_title": "Create link to share", @@ -311,6 +332,9 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", @@ -355,10 +379,10 @@ "upload_dialog_ok": "Carica", "upload_dialog_title": "Carica file", "version_announcement_overlay_ack": "Presa visione", - "version_announcement_overlay_release_notes": "note di rilascio ", + "version_announcement_overlay_release_notes": "note di rilascio", "version_announcement_overlay_text_1": "Ciao, c'è una nuova versione di", - "version_announcement_overlay_text_2": "per favore prenditi il tuo tempo per visitare il", - "version_announcement_overlay_text_3": "e verifica che il tuo docker-compose e il file .env siano aggiornati per impedire qualsiasi errore di configurazione, specialmente se utilizzate WatchTower o altri strumenti per l'aggiornamento automatico dell'applicativo", + "version_announcement_overlay_text_2": "per favore prenditi il tuo tempo per visitare le ", + "version_announcement_overlay_text_3": " e verifica che il tuo docker-compose e il file .env siano aggiornati per impedire qualsiasi errore di configurazione, specialmente se utilizzate WatchTower o altri strumenti per l'aggiornamento automatico dell'applicativo", "version_announcement_overlay_title": "Nuova versione del server disponibile \uD83C\uDF89", "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", diff --git a/mobile/assets/i18n/ja-JP.json b/mobile/assets/i18n/ja-JP.json index de2b23e06..07658e444 100644 --- a/mobile/assets/i18n/ja-JP.json +++ b/mobile/assets/i18n/ja-JP.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "タイトル変更の失敗", "album_viewer_appbar_share_leave": "アルバムから脱退", "album_viewer_appbar_share_remove": "アルバムから削除", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "ユーザーを追加", "all_people_page_title": "People", "all_videos_page_title": "ビデオ", + "app_bar_signout_dialog_content": " サインアウトしますか?", + "app_bar_signout_dialog_ok": "はい", + "app_bar_signout_dialog_title": " サインアウト", "archive_page_no_archived_assets": "アーカイブ済みの写真またはビデオがありません", "archive_page_title": "アーカイブ({})", "asset_list_layout_settings_dynamic_layout_title": "ダイナミックレイアウト", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "キャッシュ", "cache_settings_subtitle": "キャッシュの動作を変更する", "cache_settings_thumbnail_size": "サムネイルのキャッシュのサイズ ({}枚)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "キャッシュの設定", "change_password_form_confirm_password": "確定", - "change_password_form_description": "{lastaName} {firstName}さん こんにちは\n\nサーバーにアクセスするのが初めてか、パスワードリセットのリクエストがされました。新しいパスワードを入力してください", + "change_password_form_description": "{lastName} {firstName}さん こんにちは\n\nサーバーにアクセスするのが初めてか、パスワードリセットのリクエストがされました。新しいパスワードを入力してください", "change_password_form_new_password": "新しいパスワード", "change_password_form_password_mismatch": "パスワードが一致しません", "change_password_form_reenter_new_password": "再度パスワードを入力してください", @@ -148,8 +154,8 @@ "delete_dialog_cancel": "キャンセル", "delete_dialog_ok": "削除", "delete_dialog_title": "永久的に削除", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_content": "本当にこの共有リンクを消しますか?", + "delete_shared_link_dialog_title": "共有リンクを消す", "description_input_hint_text": "説明を追加", "description_input_submit_error": "説明の編集に失敗、詳細の確認はログで行ってください", "exif_bottom_sheet_description": "説明を追加", @@ -164,10 +170,15 @@ "home_page_add_to_album_conflicts": "{album}に{added}枚写真を追加しました。追加済みの{failed}枚はスキップしました。", "home_page_add_to_album_err_local": "まだアップロードされてない項目はアルバムに登録できません", "home_page_add_to_album_success": "{album}に{added}枚写真を追加しました", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "まだアップロードされてない項目はアーカイブできません", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "タイムライン構築中", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "まだアップロードされてない項目はお気に入り登録できません", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "はじめてアプリを使う場合、タイムラインに写真を表示するためにアルバムを選択してください", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "image_viewer_page_state_provider_download_error": "ダウンロード失敗", "image_viewer_page_state_provider_download_success": "ダウンロード成功", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "無視して続行", "permission_onboarding_get_started": "はじめる", "permission_onboarding_go_to_settings": "システム設定", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Immichは写真へのアクセス許可が必要です", "profile_drawer_app_logs": "ログ", "profile_drawer_client_server_up_to_date": "すべて最新です", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "設定", "profile_drawer_sign_out": "サインアウト", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "アルバム作成に失敗", "select_user_for_sharing_page_share_suggestions": "ユーザ一覧", "server_info_box_app_version": "アプリVer.", + "server_info_box_server_url": " サーバのURL", "server_info_box_server_version": "サーバーVer.", "setting_image_viewer_help": "写真をタップするとサムネイル・中画質(要設定)・オリジナル(要設定)の順に読み込みます", "setting_image_viewer_original_subtitle": "オリジナルの画像を表示したい時にオンにしてください(最大画質で表示されるのでモバイルデータとストレージの消費量が増えます)。", @@ -300,20 +315,29 @@ "share_add_photos": "写真を追加", "share_add_title": "タイトルを追加", "share_create_album": "アルバムを作成", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "準備中", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", + "shared_link_app_bar_title": "共有リンク", + "shared_link_create_app_bar_title": "共有リンクを作る", "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", + "shared_link_create_submit_button": "リンクを作る", "shared_link_edit_allow_download": "Allow public user to download", "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", + "shared_link_edit_app_bar_title": " リンクを編集する", "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", + "shared_link_edit_description": " デスクリプション ", "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": " パスワード", + "shared_link_edit_password_hint": "共有パスワードを入力する", + "shared_link_edit_show_meta": " メタデータを見る", + "shared_link_edit_submit_button": "リンクをアップデートする", + "shared_link_empty": "共有リンクはありません ", "shared_link_manage_links": "Manage Shared links", "share_done": "Done", "share_invite": "アルバムに招待", diff --git a/mobile/assets/i18n/ko-KR.json b/mobile/assets/i18n/ko-KR.json index d2786e35c..4fa57d983 100644 --- a/mobile/assets/i18n/ko-KR.json +++ b/mobile/assets/i18n/ko-KR.json @@ -1,21 +1,21 @@ { "add_to_album_bottom_sheet_added": "{album}에 추가", "add_to_album_bottom_sheet_already_exists": "{album}에 이미 포함되어 있습니다", - "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", - "advanced_settings_prefer_remote_title": "Prefer remote images", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", - "advanced_settings_tile_subtitle": "Advanced user's settings", - "advanced_settings_tile_title": "Advanced", - "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", - "advanced_settings_troubleshooting_title": "Troubleshooting", + "advanced_settings_prefer_remote_subtitle": "일부 디바이스에서는 디바이스에 있는 미디어의 썸네일을 로드하는 속도가 매우 느립니다. 대신 원격 이미지를 로드하려면 이 설정을 활성화하세요", + "advanced_settings_prefer_remote_title": "원격 이미지 선호", + "advanced_settings_self_signed_ssl_subtitle": "서버 엔드포인트에 대한 SSL 인증서 확인을 건너뜁니다. 자체 서명 인증서에 필요합니다", + "advanced_settings_self_signed_ssl_title": "자체 서명된 SSL 인증서 허용", + "advanced_settings_tile_subtitle": "고급 사용자 설정", + "advanced_settings_tile_title": "고급", + "advanced_settings_troubleshooting_subtitle": "문제 해결을 위한 추가 기능 사용", + "advanced_settings_troubleshooting_title": "문제 해결", "album_info_card_backup_album_excluded": "제외됨", "album_info_card_backup_album_included": "포함됨", "album_thumbnail_card_item": "1개 항목", "album_thumbnail_card_items": "{}개 항목", "album_thumbnail_card_shared": " · 공유", - "album_thumbnail_owned": "Owned", - "album_thumbnail_shared_by": "Shared by {}", + "album_thumbnail_owned": "소유", + "album_thumbnail_shared_by": "공유자 {}", "album_viewer_appbar_share_delete": "앨범 삭제", "album_viewer_appbar_share_err_delete": "앨범 삭제 실패", "album_viewer_appbar_share_err_leave": "앨범에서 나가지 못했습니다", @@ -23,14 +23,18 @@ "album_viewer_appbar_share_err_title": "앨범 제목 변경 실패", "album_viewer_appbar_share_leave": "앨범 나가기", "album_viewer_appbar_share_remove": "앨범에서 제거", + "album_viewer_appbar_share_to": "공유 대상", "album_viewer_page_share_add_users": "사용자 추가", - "all_people_page_title": "People", - "all_videos_page_title": "Videos", - "archive_page_no_archived_assets": "No archived assets found", - "archive_page_title": "Archive ({})", + "all_people_page_title": "사람", + "all_videos_page_title": "동영상", + "app_bar_signout_dialog_content": "정말 로그아웃하시겠습니까?", + "app_bar_signout_dialog_ok": "네", + "app_bar_signout_dialog_title": "로그 아웃", + "archive_page_no_archived_assets": "보관된 미디어를 찾을 수 없습니다", + "archive_page_title": "보관 ({})", "asset_list_layout_settings_dynamic_layout_title": "다이나믹 레이아웃", - "asset_list_layout_settings_group_automatically": "Automatic", - "asset_list_layout_settings_group_by": "다음으로 그룹화", + "asset_list_layout_settings_group_automatically": "자동", + "asset_list_layout_settings_group_by": "다음으로 미디어 그룹화", "asset_list_layout_settings_group_by_month": "월", "asset_list_layout_settings_group_by_month_day": "월 + 일", "asset_list_settings_subtitle": "사진 배열 레이아웃 설정", @@ -50,9 +54,9 @@ "backup_background_service_in_progress_notification": "미디어파일 백업 중...", "backup_background_service_upload_failure_notification": "{} 업로드 실패", "backup_controller_page_albums": "백업대상", - "backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.", - "backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled", - "backup_controller_page_background_app_refresh_enable_button_text": "Go to settings", + "backup_controller_page_background_app_refresh_disabled_content": "백그라운드 백업을 사용하려면 설정 > 일반 > 백그라운드 앱 새로 고침에서 백그라운드 앱 새로 고침을 활성화합니다", + "backup_controller_page_background_app_refresh_disabled_title": "백그라운드 앱 새로 고침 비활성화", + "backup_controller_page_background_app_refresh_enable_button_text": "설정으로 이동", "backup_controller_page_background_battery_info_link": "사용 가이드", "backup_controller_page_background_battery_info_message": "최상의 백업 환경을 위해 Immich 앱의 백그라운드 활동을 제한하는 배터리 최적화기능을 꺼주세요.\n\n휴대폰마다 설정방법이 다르므로 제조업체별로 설정방법을 확인하세요.", "backup_controller_page_background_battery_info_ok": "확인", @@ -94,11 +98,11 @@ "backup_controller_page_uploading_file_info": "파일 정보 업로드 중", "backup_err_only_album": "유일한 앨범은 제거할 수 없습니다", "backup_info_card_assets": "미디어", - "backup_manual_cancelled": "Cancelled", - "backup_manual_failed": "Failed", - "backup_manual_in_progress": "Upload already in progress. Try after sometime", - "backup_manual_success": "Success", - "backup_manual_title": "Upload status", + "backup_manual_cancelled": "취소됨", + "backup_manual_failed": "실패", + "backup_manual_in_progress": "업로드가 이미 진행 중입니다. 잠시 후 시도하세요", + "backup_manual_success": "성공", + "backup_manual_title": "업로드 상태", "cache_settings_album_thumbnails": "라이브러리 페이지 썸네일 ({} 미디어)", "cache_settings_clear_cache_button": "캐시 지우기", "cache_settings_clear_cache_button_title": "앱의 캐시를 지웁니다. 이 작업은 캐시가 다시 빌드될 때까지 앱의 성능에 상당한 영향을 미칩니다.", @@ -111,36 +115,38 @@ "cache_settings_statistics_title": "캐시 사용률", "cache_settings_subtitle": "Immich 앱의 캐싱 동작 제어", "cache_settings_thumbnail_size": "썸네일 캐시 크기 ({} 미디어)", + "cache_settings_tile_subtitle": "로컬 저장소 동작 제어", + "cache_settings_tile_title": "로컬 저장소", "cache_settings_title": "캐시 설정", "change_password_form_confirm_password": "비밀번호 확인", - "change_password_form_description": "{firstName} {lastName} 님, 안녕하세요.\n\n시스템에 처음 로그인했거나 비밀번호 변경 요청이 있었습니다. 아래에 새 비밀번호를 입력하세요.", + "change_password_form_description": "{name} 님, 안녕하세요.\n\n시스템에 처음 로그인했거나 비밀번호 변경 요청이 있었습니다. 아래에 새 비밀번호를 입력하세요.", "change_password_form_new_password": "새 비밀번호", "change_password_form_password_mismatch": "비밀번호가 일치하지 않습니다", "change_password_form_reenter_new_password": "새 비밀번호 재입력", "common_add_to_album": "앨범에 추가", "common_change_password": "비밀번호 변경", "common_create_new_album": "새 앨범 만들기", - "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.", + "common_server_error": "네트워크 연결을 확인하고 서버에 연결할 수 있는지, 앱/서버 버전이 호환되는지 확인하세요", "common_shared": "공유됨", "control_bottom_app_bar_add_to_album": "앨범에 추가", "control_bottom_app_bar_album_info": "{} 항목", "control_bottom_app_bar_album_info_shared": "{} 항목 · 공유됨", - "control_bottom_app_bar_archive": "Archive", + "control_bottom_app_bar_archive": "보관", "control_bottom_app_bar_create_new_album": "앨범 생성", "control_bottom_app_bar_delete": "삭제", "control_bottom_app_bar_favorite": "즐겨찾기", "control_bottom_app_bar_share": "공유", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", - "control_bottom_app_bar_unarchive": "Unarchive", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_share_to": "공유 대상", + "control_bottom_app_bar_stack": "스택", + "control_bottom_app_bar_unarchive": "보관 해제", + "control_bottom_app_bar_upload": "업로드", "create_album_page_untitled": "제목없음", "create_shared_album_page_create": "만들기", "create_shared_album_page_share": "공유", "create_shared_album_page_share_add_assets": "사진 추가", "create_shared_album_page_share_select_photos": "사진 선택", - "curated_location_page_title": "Places", - "curated_object_page_title": "Things", + "curated_location_page_title": "장소", + "curated_object_page_title": "사물", "daily_title_text_date": "E, M월 d일", "daily_title_text_date_year": "E, M월 d일, yyyy", "date_format": "yyyy년 M월 d일, EEEE • a h:mm", @@ -148,10 +154,10 @@ "delete_dialog_cancel": "취소", "delete_dialog_ok": "삭제", "delete_dialog_title": "영구적으로 삭제", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", - "description_input_hint_text": "Add description...", - "description_input_submit_error": "Error updating description, check the log for more details", + "delete_shared_link_dialog_content": "이 공유 링크를 삭제하시겠습니까?", + "delete_shared_link_dialog_title": "공유 링크 삭제", + "description_input_hint_text": "설명 추가", + "description_input_submit_error": "설명 업데이트 오류, 자세한 내용은 로그를 확인하세요", "exif_bottom_sheet_description": "설명 추가...", "exif_bottom_sheet_details": "상세정보", "exif_bottom_sheet_location": "위치", @@ -159,31 +165,36 @@ "experimental_settings_new_asset_list_title": "실험적 사진 그리드 적용", "experimental_settings_subtitle": "문제시 책임지지 않습니다!", "experimental_settings_title": "실험적기능", - "favorites_page_no_favorites": "No favorite assets found", + "favorites_page_no_favorites": "즐겨찾기된 미디어를 찾을 수 없습니다", "favorites_page_title": "즐겨찾기", "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_archive_err_local": "Can not archive local assets yet, skipping", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", + "home_page_archive_err_local": "아직 로컬 미디어를 보관할 수 없습니다", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "타임라인 생성", - "home_page_favorite_err_local": "미디어파일을 즐겨찾기에 추가할 수 없어, 건너뜁니다.", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", + "home_page_favorite_err_local": "아직 로컬 미디어를 즐겨찾기에 추가할 수 없으므로 건너뜁니다", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "앱을 처음 사용하는 경우 타임라인이 앨범의 사진과 비디오를 채울 수 있도록 백업대상 앨범을 선택해야 합니다.", - "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", + "home_page_share_err_local": "Can not share local assets via link, skipping", + "home_page_upload_err_limit": "한번에 최대 30개의 미디어만 업로드할 수 있습니다", "image_viewer_page_state_provider_download_error": "다운로드 에러", "image_viewer_page_state_provider_download_success": "다운로드 완료", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "공유 오류", "library_page_albums": "앨범", - "library_page_archive": "Archive", - "library_page_device_albums": "Albums on Device", + "library_page_archive": "보관", + "library_page_device_albums": "장치의 앨범", "library_page_favorites": "즐겨찾기", "library_page_new_album": "새 앨범", "library_page_sharing": "공유", "library_page_sort_created": "최근생성일", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "마지막 수정", + "library_page_sort_most_recent_photo": "가장 최근 사진", "library_page_sort_title": "앨범 제목", - "login_disabled": "Login has been disabled", - "login_form_api_exception": "API exception. Please check the server URL and try again.", + "login_disabled": "로그인이 비활성화되었습니다", + "login_form_api_exception": "API 예외입니다. 서버 URL을 확인한 후 다시 시도하세요", "login_form_button_text": "로그인", "login_form_email_hint": "youremail@email.com", "login_form_endpoint_hint": "https://your-server-ip:port/api", @@ -196,86 +207,90 @@ "login_form_failed_get_oauth_server_config": "OAuth 로그인 오류, 서버 URL을 확인해주세요", "login_form_failed_get_oauth_server_disable": "이 서버에서는 OAuth 기능을 사용할 수 없습니다.", "login_form_failed_login": "로그인 오류, 서버 URL, 이메일 및 비밀번호를 확인하세요", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", + "login_form_handshake_exception": "서버에 핸드셰이크 예외가 발생했습니다. 자체 서명 인증서를 사용하는 경우 설정에서 자체 서명 인증서 지원을 사용 설정합니다", "login_form_label_email": "이메일", "login_form_label_password": "비밀번호", - "login_form_next_button": "Next", + "login_form_next_button": "다음", "login_form_password_hint": "비밀번호", "login_form_save_login": "로그인상태 유지", - "login_form_server_empty": "Enter a server URL.", - "login_form_server_error": "Could not connect to server.", - "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", - "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_cancel": "Cancel", - "map_location_dialog_yes": "Yes", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", - "map_location_service_disabled_title": "Location Service disabled", - "map_no_assets_in_bounds": "No photos in this area", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", - "map_no_location_permission_title": "Location Permission denied", - "map_settings_dark_mode": "Dark mode", - "map_settings_dialog_cancel": "Cancel", - "map_settings_dialog_save": "Save", - "map_settings_dialog_title": "Map Settings", - "map_settings_include_show_archived": "Include Archived", - "map_settings_only_relative_range": "Date range", - "map_settings_only_show_favorites": "Show Favorite Only", - "map_zoom_to_see_photos": "Zoom out to see photos", + "login_form_server_empty": "서버 URL 입력", + "login_form_server_error": "서버에 연결할 수 없습니다", + "login_password_changed_error": "비밀번호를 업데이트하는 동안 오류가 발생했습니다", + "login_password_changed_success": "비밀번호 업데이트 성공", + "map_cannot_get_user_location": "사용자 위치를 가져올 수 없습니다.", + "map_location_dialog_cancel": "아니오", + "map_location_dialog_yes": "예", + "map_location_service_disabled_content": "현재 위치의 미디어를 표시하려면 위치 서비스를 활성화해야 합니다. 지금 활성화하시겠습니까?", + "map_location_service_disabled_title": "위치 서비스 비활성화", + "map_no_assets_in_bounds": "이 영역에 사진이 없습니다", + "map_no_location_permission_content": "현재 위치의 미디어를 표시하려면 위치 권한이 필요합니다. 지금 허용하시겠습니까?", + "map_no_location_permission_title": "위치 권한 거부됨", + "map_settings_dark_mode": "다크 모드", + "map_settings_dialog_cancel": "취소", + "map_settings_dialog_save": "저장", + "map_settings_dialog_title": "지도 설정", + "map_settings_include_show_archived": "아카이브 포함", + "map_settings_only_relative_range": "날짜 범위", + "map_settings_only_show_favorites": "즐겨찾기에만 표시", + "map_zoom_to_see_photos": "축소하여 사진 보기", "monthly_title_text_date_format": "y년 M월", - "motion_photos_page_title": "Motion Photos", + "motion_photos_page_title": "모션 사진", "notification_permission_dialog_cancel": "취소", "notification_permission_dialog_content": "알림을 활성화하려면 설정으로 이동하여 허용을 선택해주세요.", "notification_permission_dialog_settings": "설정", "notification_permission_list_tile_content": "알림 활성화 권한허용", "notification_permission_list_tile_enable_button": "알림 활성화", "notification_permission_list_tile_title": "알림 권한", - "partner_page_add_partner": "Add partner", - "partner_page_empty_message": "Your photos are not yet shared with any partner.", - "partner_page_no_more_users": "No more users to add", - "partner_page_partner_add_failed": "Failed to add partner", - "partner_page_select_partner": "Select partner", - "partner_page_shared_to_title": "Shared to", - "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", - "partner_page_stop_sharing_title": "Stop sharing your photos?", - "partner_page_title": "Partner", - "permission_onboarding_continue_anyway": "Continue anyway", - "permission_onboarding_get_started": "Get started", - "permission_onboarding_go_to_settings": "Go to settings", - "permission_onboarding_grant_permission": "Grant permission", - "permission_onboarding_log_out": "Log out", - "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.", - "permission_onboarding_permission_granted": "Permission granted! You are all set.", - "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", - "permission_onboarding_request": "Immich requires permission to view your photos and videos.", + "partner_page_add_partner": "파트너 추가", + "partner_page_empty_message": "사진이 아직 어떤 파트너와도 공유되지 않았습니다", + "partner_page_no_more_users": "더 이상 추가할 사용자 없음", + "partner_page_partner_add_failed": "파트너 추가에 실패했습니다", + "partner_page_select_partner": "파트너 선택", + "partner_page_shared_to_title": "공유 대상", + "partner_page_stop_sharing_content": "더 이상 {}에서 사진에 액세스할 수 없습니다.", + "partner_page_stop_sharing_title": "사진 공유를 중단하시겠습니까?", + "partner_page_title": "파트너", + "permission_onboarding_back": "Back", + "permission_onboarding_continue_anyway": "어쨌든 계속하기", + "permission_onboarding_get_started": "시작하기", + "permission_onboarding_go_to_settings": "설정으로 이동", + "permission_onboarding_grant_permission": "권한 부여", + "permission_onboarding_log_out": "로그 아웃", + "permission_onboarding_permission_denied": "권한이 거부되었습니다. Immich를 사용하려면 설정에서 사진 및 동영상 권한을 부여하세요", + "permission_onboarding_permission_granted": "승인되었습니다! 모든 준비가 완료되었습니다", + "permission_onboarding_permission_limited": "권한 제한. Immich가 전체 갤러리 컬렉션을 백업하고 관리하도록 하려면 설정에서 사진 및 동영상 권한을 부여하세요", + "permission_onboarding_request": "Immich는 사진과 동영상을 볼 수 있는 권한을 요구합니다", "profile_drawer_app_logs": "로그", "profile_drawer_client_server_up_to_date": "클라이언트와 서버가 최신 상태입니다", + "profile_drawer_documentation": "문서", + "profile_drawer_github": "깃허브", "profile_drawer_settings": "설정", "profile_drawer_sign_out": "로그아웃", - "profile_drawer_trash": "Trash", - "recently_added_page_title": "Recently Added", + "profile_drawer_trash": "휴지통", + "recently_added_page_title": "최근 추가", "search_bar_hint": "사진 검색", - "search_page_categories": "Categories", - "search_page_favorites": "Favorites", - "search_page_motion_photos": "Motion Photos", + "search_page_categories": "카테고리", + "search_page_favorites": "즐겨찾기", + "search_page_motion_photos": "모션 사진", "search_page_no_objects": "발견된 사물이\n없습니다", "search_page_no_places": "발견된 장소가\n없습니다", - "search_page_people": "People", + "search_page_people": "사람", "search_page_places": "장소", - "search_page_recently_added": "Recently added", - "search_page_screenshots": "Screenshots", - "search_page_selfies": "Selfies", + "search_page_recently_added": "최근 추가", + "search_page_screenshots": "스크린샷", + "search_page_selfies": "셀카", "search_page_things": "사물", - "search_page_videos": "Videos", - "search_page_view_all_button": "View all", - "search_page_your_activity": "Your activity", + "search_page_videos": "동영상", + "search_page_view_all_button": "모두 보기", + "search_page_your_activity": "내 활동", "search_result_page_new_search_hint": "새 검색", - "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ", + "search_suggestion_list_smart_search_hint_1": "스마트 검색은 기본적으로 활성화되어 있으며, 메타데이터를 검색하려면 다음 구문을 사용합니다", "search_suggestion_list_smart_search_hint_2": "m:your-search-term", "select_additional_user_for_sharing_page_suggestions": "초대 가능한 사용자 제안", "select_user_for_sharing_page_err_album": "앨범 생성 실패", "select_user_for_sharing_page_share_suggestions": "초대 가능한 사용자 제안", "server_info_box_app_version": "앱 버전", + "server_info_box_server_url": "서버 URL", "server_info_box_server_version": "서버 버전", "setting_image_viewer_help": "상세뷰어는 먼저 작은 썸네일을 불러온 다음 중간크기 미리보기를 불러오고(활성화된 경우) 마지막으로 원본을 불러옵니다(활성화된 경우).", "setting_image_viewer_original_subtitle": "원본 해상도 이미지(고화질)를 로드하려면 활성화합니다. 데이터 사용량을 줄이려면 비활성화합니다.", @@ -300,28 +315,37 @@ "share_add_photos": "사진 추가", "share_add_title": "새 앨범제목", "share_create_album": "앨범 만들기", + "shared_album_activities_input_disable": "댓글이 비활성화되었습니다.", + "shared_album_activities_input_hint": "말하기", + "shared_album_activity_remove_content": "이 활동을 삭제하시겠습니까?", + "shared_album_activity_remove_title": "활동 삭제", + "shared_album_activity_setting_subtitle": "다른 사람이 응답하도록 허용", + "shared_album_activity_setting_title": "댓글 및 좋아요", "share_dialog_preparing": "준비중...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_app_bar_title": "공유 링크", + "shared_link_create_app_bar_title": "공유할 링크 만들기", + "shared_link_create_info": "링크를 가진 모든 사람이 선택한 사진을 볼 수 있도록 합니다", + "shared_link_create_submit_button": "링크 만들기", + "shared_link_edit_allow_download": "공용 사용자의 다운로드 허용", + "shared_link_edit_allow_upload": "공용 사용자의 업로드 허용", + "shared_link_edit_app_bar_title": "링크 수정", + "shared_link_edit_change_expiry": "만료 시간 변경", + "shared_link_edit_description": "설명", + "shared_link_edit_description_hint": "공유 설명 입력", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "비밀번호", + "shared_link_edit_password_hint": "공유 비밀번호 입력", + "shared_link_edit_show_meta": "메타데이터 표시", + "shared_link_edit_submit_button": "링크 업데이트", + "shared_link_empty": "공유 링크가 없습니다", + "shared_link_manage_links": "공유 링크 관리", + "share_done": "완료", "share_invite": "앨범에 초대", "sharing_page_album": "공유앨범", "sharing_page_description": "공유앨범을 만들어 다른 사용자들과 사진 및 비디오를 공유합니다.", "sharing_page_empty_list": "공유앨범 없음", "sharing_silver_appbar_create_shared_album": "공유앨범 만들기", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "공유 링크", "sharing_silver_appbar_share_partner": "파트너와 공유", "tab_controller_nav_library": "라이브러리", "tab_controller_nav_photos": "사진", @@ -337,30 +361,30 @@ "theme_setting_theme_title": "테마", "theme_setting_three_stage_loading_subtitle": "이 기능은 로딩 성능을 향상시킬 수 있지만 훨씬 더 많은 데이터를 사용합니다.", "theme_setting_three_stage_loading_title": "3단계 로딩 활성화", - "translated_text_options": "Options", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", - "upload_dialog_cancel": "Cancel", - "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", - "upload_dialog_ok": "Upload", - "upload_dialog_title": "Upload Asset", + "translated_text_options": "옵션", + "trash_page_delete": "삭제", + "trash_page_delete_all": "모두 삭제", + "trash_page_empty_trash_btn": "휴지통 비우기", + "trash_page_empty_trash_dialog_content": "휴지통에 버린 미디어를 비우고 싶으신가요? 이 항목들은 Immich에서 영구적으로 삭제됩니다", + "trash_page_empty_trash_dialog_ok": "확인", + "trash_page_info": "휴지통에 버린 항목은 {}일 후에 영구 삭제됩니다", + "trash_page_no_assets": "휴지통에 버려진 미디어 없음", + "trash_page_restore": "복원", + "trash_page_restore_all": "모두 복원", + "trash_page_select_assets_btn": "미디어 선택", + "trash_page_select_btn": "선택", + "trash_page_title": "휴지통 ({})", + "upload_dialog_cancel": "취소", + "upload_dialog_info": "선택한 미디어를 서버에 백업하시겠습니까?", + "upload_dialog_ok": "업로드", + "upload_dialog_title": "미디어 업로드", "version_announcement_overlay_ack": "승인", "version_announcement_overlay_release_notes": "릴리스 정보", "version_announcement_overlay_text_1": "안녕하세요!", "version_announcement_overlay_text_2": "앱에 새로운 업데이트가 있습니다!", "version_announcement_overlay_text_3": "특히 WatchTower 또는 서버 응용 프로그램 자동 업데이트를 처리하는 메커니즘을 사용하는 경우 잘못된 구성을 방지하기 위해 docker-compose 및 .env 설정이 최신 상태인지 확인하세요.", "version_announcement_overlay_title": "새 서버 버전 사용 가능 \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "스택에서 제거", + "viewer_stack_use_as_main_asset": "메인 미디어로 사용", + "viewer_unstack": "스택 해제" } \ No newline at end of file diff --git a/mobile/assets/i18n/lv-LV.json b/mobile/assets/i18n/lv-LV.json index 966bc8db8..875044c29 100644 --- a/mobile/assets/i18n/lv-LV.json +++ b/mobile/assets/i18n/lv-LV.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Neizdevās mainīt albuma nosaukumu", "album_viewer_appbar_share_leave": "Pamest albumu", "album_viewer_appbar_share_remove": "Noņemt no albuma", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Pievienot lietotājus", "all_people_page_title": "Cilvēki", "all_videos_page_title": "Videoklipi", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "Nav atrasts neviens arhivēts aktīvs", "archive_page_title": "Arhīvs ({})", "asset_list_layout_settings_dynamic_layout_title": "Dinamiskais izkārtojums", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Kešatmiņas lietojums", "cache_settings_subtitle": "Kontrolēt Immich mobilās lietotnes kešdarbi", "cache_settings_thumbnail_size": "Sīktēlu keša lielums ({} aktīvi)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Kešdarbes iestatījumi", "change_password_form_confirm_password": "Apstiprināt Paroli", - "change_password_form_description": "Sveiki {FirstName} {LastName},\n\nŠī ir pirmā reize, kad pierakstāties sistēmā, vai arī ir iesniegts pieprasījums mainīt paroli. Lūdzu, zemāk ievadiet jauno paroli.", + "change_password_form_description": "Sveiki {name},\n\nŠī ir pirmā reize, kad pierakstāties sistēmā, vai arī ir iesniegts pieprasījums mainīt paroli. Lūdzu, zemāk ievadiet jauno paroli.", "change_password_form_new_password": "Jauna Parole", "change_password_form_password_mismatch": "Paroles nesakrīt", "change_password_form_reenter_new_password": "Atkārtoti ievadīt jaunu paroli", @@ -164,10 +170,15 @@ "home_page_add_to_album_conflicts": "Pievienoja {added} aktīvus albumam {album}. {failed} aktīvi jau ir albumā.", "home_page_add_to_album_err_local": "Albumiem vēl nevar pievienot lokālos aktīvus, notiek izlaišana", "home_page_add_to_album_success": "Pievienoja {added} aktīvus albumam {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Vēl nevar arhivēt lokālos aktīvus, notiek izlaišana", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Tiek izveidota laika skala", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Vēl nevar pievienot izlaisei vietējos aktīvus, notiek izlaišana", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Ja šī ir pirmā reize, kad izmantojat aplikāciju, lūdzu, izvēlieties dublējuma albumu(s), lai laika skala varētu aizpildīt fotoattēlus un videoklipus albumā(os).", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "image_viewer_page_state_provider_download_error": "Lejupielādes Kļūda", "image_viewer_page_state_provider_download_success": "Lejupielāde Izdevās", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} vairs nevarēs piekļūt jūsu fotoattēliem.", "partner_page_stop_sharing_title": "Beigt kopīgot jūsu fotogrāfijas?", "partner_page_title": "Partneris", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Tomēr turpināt", "permission_onboarding_get_started": "Darba sākšana", "permission_onboarding_go_to_settings": "Doties uz iestatījumiem", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Immich nepieciešama atļauja skatīt jūsu fotoattēlus un videoklipus.", "profile_drawer_app_logs": "Žurnāli", "profile_drawer_client_server_up_to_date": "Klients un serveris ir atjaunināti", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Iestatījumi", "profile_drawer_sign_out": "Izrakstīties", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Neizdevās izveidot albumu", "select_user_for_sharing_page_share_suggestions": "Ieteikumi", "server_info_box_app_version": "Aplikācijas Versija", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "Servera Versija", "setting_image_viewer_help": "Detaļu skatītājs vispirms ielādē mazo sīktēlu, pēc tam ielādē vidēja lieluma priekšskatījumu (ja iespējots), visbeidzot ielādē oriģinālu (ja iespējots).", "setting_image_viewer_original_subtitle": "Iespējojiet sākotnējā pilnas izšķirtspējas attēla (liels!) ielādi. Atspējot lai samazinātu datu lietojumu (gan tīklā, gan ierīces kešatmiņā).", @@ -300,6 +315,12 @@ "share_add_photos": "Pievienot fotoattēlus", "share_add_title": "Pievienot virsrakstu", "share_create_album": "Izveidot albumu", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Notiek sagatavošana...", "shared_link_app_bar_title": "Shared Links", "shared_link_create_app_bar_title": "Create link to share", @@ -311,6 +332,9 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", diff --git a/mobile/assets/i18n/mn.json b/mobile/assets/i18n/mn.json index b20d35a7d..dfbb54d89 100644 --- a/mobile/assets/i18n/mn.json +++ b/mobile/assets/i18n/mn.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Failed to change album title", "album_viewer_appbar_share_leave": "Leave album", "album_viewer_appbar_share_remove": "Remove from album", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Add users", "all_people_page_title": "People", "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", "archive_page_title": "Archive ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Cache usage", "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", @@ -164,10 +170,15 @@ "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", "home_page_add_to_album_success": "Added {added} assets to album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Can not archive local assets yet, skipping", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Building the timeline", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "image_viewer_page_state_provider_download_error": "Download Error", "image_viewer_page_state_provider_download_success": "Download Success", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Get started", "permission_onboarding_go_to_settings": "Go to settings", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "profile_drawer_app_logs": "Logs", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Settings", "profile_drawer_sign_out": "Sign Out", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Failed to create album", "select_user_for_sharing_page_share_suggestions": "Suggestions", "server_info_box_app_version": "App Version", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "Server Version", "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", @@ -300,6 +315,12 @@ "share_add_photos": "Add photos", "share_add_title": "Add a title", "share_create_album": "Create album", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Preparing...", "shared_link_app_bar_title": "Shared Links", "shared_link_create_app_bar_title": "Create link to share", @@ -311,6 +332,9 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", diff --git a/mobile/assets/i18n/nb-NO.json b/mobile/assets/i18n/nb-NO.json index 18cf4475b..cd2e9be27 100644 --- a/mobile/assets/i18n/nb-NO.json +++ b/mobile/assets/i18n/nb-NO.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Feilet ved endring av albumtittel", "album_viewer_appbar_share_leave": "Forlat album", "album_viewer_appbar_share_remove": "Fjern fra album", + "album_viewer_appbar_share_to": "Del til", "album_viewer_page_share_add_users": "Legg til brukere", "all_people_page_title": "Folk", "all_videos_page_title": "Videoer", + "app_bar_signout_dialog_content": "Er du sikker på at du vil logge ut?", + "app_bar_signout_dialog_ok": "Ja", + "app_bar_signout_dialog_title": "Logg ut", "archive_page_no_archived_assets": "Ingen arkiverte objekter funnet", "archive_page_title": "Arkiv ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamisk bildeorganisering", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Bufferbruk", "cache_settings_subtitle": "Kontroller bufringsadferden til Immich-appen", "cache_settings_thumbnail_size": "Størrelse på miniatyrbildebuffer ({} objekter)", + "cache_settings_tile_subtitle": "Kontroller lokal lagring", + "cache_settings_tile_title": "Lokal lagring", "cache_settings_title": "Bufringsinnstillinger", "change_password_form_confirm_password": "Bekreft passord", - "change_password_form_description": "Hei {firstName} {lastName}!\n\nDette er enten første gang du logger på systemet, eller det er sendt en forespørsel om å endre passordet ditt. Vennligst skriv inn det nye passordet nedenfor.", + "change_password_form_description": "Hei {name}!\n\nDette er enten første gang du logger på systemet, eller det er sendt en forespørsel om å endre passordet ditt. Vennligst skriv inn det nye passordet nedenfor.", "change_password_form_new_password": "Nytt passord", "change_password_form_password_mismatch": "Passordene stemmer ikke", "change_password_form_reenter_new_password": "Skriv nytt passord igjen", @@ -130,10 +136,10 @@ "control_bottom_app_bar_delete": "Slett", "control_bottom_app_bar_favorite": "Favoritt", "control_bottom_app_bar_share": "Del", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_share_to": "Del til", + "control_bottom_app_bar_stack": "Stable", "control_bottom_app_bar_unarchive": "Fjern fra arkiv", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "Last opp", "create_album_page_untitled": "Uten navn", "create_shared_album_page_create": "Opprett", "create_shared_album_page_share": "Del", @@ -148,8 +154,8 @@ "delete_dialog_cancel": "Avbryt", "delete_dialog_ok": "Slett", "delete_dialog_title": "Slett permanent", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_content": "Er du sikker på at du vil slette denne delte linken?", + "delete_shared_link_dialog_title": "Slett delt link", "description_input_hint_text": "Legg til beskrivelse ...", "description_input_submit_error": "Feil ved oppdatering av beskrivelse, sjekk loggen for flere detaljer", "exif_bottom_sheet_description": "Legg til beskrivelse ...", @@ -164,14 +170,19 @@ "home_page_add_to_album_conflicts": "Lagt til {added} objekter til album {album}. {failed} objekter er allerede i albumet.", "home_page_add_to_album_err_local": "Kan ikke legge til lokale objekter til album enda, hopper over", "home_page_add_to_album_success": "Lagt til {added} objekter til album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Kan ikke arkivere lokale objekter enda, hopper over", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Genererer tidslinjen", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Kan ikke sette favoritt på lokale objekter enda, hopper over", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Hvis dette er første gangen du benytter appen, velg et album (eller flere) for sikkerhetskopiering, slik at tidslinjen kan fylles med dine bilder og videoer.", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Maksimalt 30 objekter kan lastes opp om gangen, hopper over", "image_viewer_page_state_provider_download_error": "Nedlasting feilet", "image_viewer_page_state_provider_download_success": "Nedlasting vellykket", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "Delingsfeil", "library_page_albums": "Albumer", "library_page_archive": "Arkiv", "library_page_device_albums": "Albumer på enheten", @@ -179,8 +190,8 @@ "library_page_new_album": "Nytt album", "library_page_sharing": "Deling", "library_page_sort_created": "Nylig opplastet", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "Sist endret", + "library_page_sort_most_recent_photo": "Siste bilde", "library_page_sort_title": "Albumtittel", "login_disabled": "Innlogging har blitt deaktivert", "login_form_api_exception": "API-feil. Sjekk URL-en til serveren og prøv igjen.", @@ -218,7 +229,7 @@ "map_settings_dialog_cancel": "Avbryt", "map_settings_dialog_save": "Lagre", "map_settings_dialog_title": "Kartinnstillinger", - "map_settings_include_show_archived": "Include Archived", + "map_settings_include_show_archived": "Inkluder arkiverte", "map_settings_only_relative_range": "Datoområde", "map_settings_only_show_favorites": "Vis kun favoritter", "map_zoom_to_see_photos": "Zoom ut for å se bilder", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} vil ikke lenger ha tilgang til dine bilder.", "partner_page_stop_sharing_title": "Stopp deling av bildene dine?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Fortsett uansett", "permission_onboarding_get_started": "Kom i gang", "permission_onboarding_go_to_settings": "Gå til innstillinger", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich trenger tilgang til å se dine bilder og videoer", "profile_drawer_app_logs": "Logg", "profile_drawer_client_server_up_to_date": "Klient og server er oppdatert", + "profile_drawer_documentation": "Dokumentasjon", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Innstillinger", "profile_drawer_sign_out": "Logg ut", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "Søppelbøtte", "recently_added_page_title": "Nylig lagt til", "search_bar_hint": "Søk i dine bilder", "search_page_categories": "Kategorier", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Feilet ved oppretting av album", "select_user_for_sharing_page_share_suggestions": "Forslag", "server_info_box_app_version": "App-versjon", + "server_info_box_server_url": "Server-adresse", "server_info_box_server_version": "Server-versjon", "setting_image_viewer_help": "Detaljvisningen laster først miniatyrbildet, deretter forhåndsvisningsbildet (hvis aktivert), og til slutt originalen (hvis aktivert).", "setting_image_viewer_original_subtitle": "Aktiver for å laste originalbildet i full oppløsning (stort!). Deaktiver for å spare databruk (både nettverksbruk og bufferdata på enheten).", @@ -300,28 +315,37 @@ "share_add_photos": "Legg til bilder", "share_add_title": "Legg til tittel", "share_create_album": "Opprett album", + "shared_album_activities_input_disable": "Kommenterer er deaktivert", + "shared_album_activities_input_hint": "Si noe", + "shared_album_activity_remove_content": "Vil du slette denne aktiviteten?", + "shared_album_activity_remove_title": "Slett aktivitet", + "shared_album_activity_setting_subtitle": "La andre respondere", + "shared_album_activity_setting_title": "Kommentarer og likes", "share_dialog_preparing": "Forbereder ...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_app_bar_title": "Delte linker", + "shared_link_create_app_bar_title": "Opprett delelink", + "shared_link_create_info": "La alle med linken se de(t) valgte bilde(ne)", + "shared_link_create_submit_button": "Opprett link", + "shared_link_edit_allow_download": "Tillat offentlig bruker å laste ned", + "shared_link_edit_allow_upload": "Tillat offentlig bruker å laste opp", + "shared_link_edit_app_bar_title": "Endre link", + "shared_link_edit_change_expiry": "Endre utløpstid", + "shared_link_edit_description": "Beskrivelse", + "shared_link_edit_description_hint": "Endre delebeskrivelse", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Passord", + "shared_link_edit_password_hint": "Skriv inn dele-passord", + "shared_link_edit_show_meta": "Vis metadata", + "shared_link_edit_submit_button": "Oppdater link", + "shared_link_empty": "Du har ingen delte linker", + "shared_link_manage_links": "Håndter delte linker", + "share_done": "Ferdig", "share_invite": "Inviter til album", "sharing_page_album": "Delte album", "sharing_page_description": "Lag delte albumer for å dele bilder og videoer med folk i nettverket ditt.", "sharing_page_empty_list": "TOM LISTE", "sharing_silver_appbar_create_shared_album": "Lag delt album", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "Delte linker", "sharing_silver_appbar_share_partner": "Del med partner", "tab_controller_nav_library": "Bibliotek", "tab_controller_nav_photos": "Bilder", @@ -338,18 +362,18 @@ "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", "translated_text_options": "Valg", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", + "trash_page_delete": "Slett", + "trash_page_delete_all": "Slett alt", + "trash_page_empty_trash_btn": "Tøm søppelbøtte", + "trash_page_empty_trash_dialog_content": "Vil du tømme søppelbøtten? Objektene vil bli permanent fjernet fra Immich", "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", + "trash_page_info": "Objekter i søppelbøtten blir permanent fjernet etter {} dager", + "trash_page_no_assets": "Ingen forkastede objekter", + "trash_page_restore": "Gjenopprett", + "trash_page_restore_all": "Gjenopprett alt", + "trash_page_select_assets_btn": "Velg objekter", + "trash_page_select_btn": "Velg", + "trash_page_title": "Søppelbøtte ({})", "upload_dialog_cancel": "Avbryt", "upload_dialog_info": "Vil du utføre backup av valgte objekt(er) til serveren?", "upload_dialog_ok": "Last opp", @@ -360,7 +384,7 @@ "version_announcement_overlay_text_2": "vennligst ta deg tid til å besøke ", "version_announcement_overlay_text_3": " og verifiser at docker-compose og .env-oppsettet ditt er oppdatert for å forhindre en eventuell feilkonfigurasjon, spesielt hvis du benytter WatchTower eller en annen tjeneste som håndterer oppdatering av server-applikasjonen automatisk.", "version_announcement_overlay_title": "Ny serverversjon tilgjengelig", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "Fjern fra stabling", + "viewer_stack_use_as_main_asset": "Bruk som hovedobjekt", + "viewer_unstack": "avstable" } \ No newline at end of file diff --git a/mobile/assets/i18n/nl-NL.json b/mobile/assets/i18n/nl-NL.json index 724b650f9..eece8bd40 100644 --- a/mobile/assets/i18n/nl-NL.json +++ b/mobile/assets/i18n/nl-NL.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Albumtitel wijzigen mislukt", "album_viewer_appbar_share_leave": "Verlaat album", "album_viewer_appbar_share_remove": "Verwijder uit album", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Gebruikers toevoegen", "all_people_page_title": "Personen", "all_videos_page_title": "Video's", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "Geen gearchiveerde items gevonden", "archive_page_title": "Archief ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamische layout", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Cachegebruik", "cache_settings_subtitle": "Beheer het cachegedrag van de Immich app", "cache_settings_thumbnail_size": "Thumbnail-cachegrootte ({} items)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Cache-instellingen", "change_password_form_confirm_password": "Bevestig wachtwoord", - "change_password_form_description": "Hallo {firstName} {lastName},\n\nDit is ofwel de eerste keer dat je inlogt, of er is een verzoek gedaan om je wachtwoord te wijzigen. Vul hieronder een nieuw wachtwoord in.", + "change_password_form_description": "Hallo {name},\n\nDit is ofwel de eerste keer dat je inlogt, of er is een verzoek gedaan om je wachtwoord te wijzigen. Vul hieronder een nieuw wachtwoord in.", "change_password_form_new_password": "Nieuw wachtwoord", "change_password_form_password_mismatch": "Wachtwoorden komen niet overeen", "change_password_form_reenter_new_password": "Vul het wachtwoord opnieuw in", @@ -164,10 +170,15 @@ "home_page_add_to_album_conflicts": "{added} items toegevoegd aan album {album}. {failed} items staan al in het album.", "home_page_add_to_album_err_local": "Lokale items kunnen nog niet aan albums worden toegevoegd, overslaan", "home_page_add_to_album_success": "{added} items toegevoegd aan album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Lokale items kunnen nog niet gearchiveerd worden, overslaan", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Tijdlijn opbouwen", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Lokale items kunnen nog niet als favoriet worden aangemerkt, overslaan", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Als dit de eerste keer is dat je de app gebruikt, zorg er dan voor dat je een back-up album kiest, zodat de tijdlijn gevuld kan worden met foto's en video's uit het album.", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Kan maximaal 30 assets tegelijk uploaden, overslaan", "image_viewer_page_state_provider_download_error": "Download mislukt", "image_viewer_page_state_provider_download_success": "Download succesvol", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} zal geen toegang meer hebben tot je fotos's.", "partner_page_stop_sharing_title": "Stoppen met het delen van je foto's?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Toch doorgaan", "permission_onboarding_get_started": "Aan de slag", "permission_onboarding_go_to_settings": "Ga naar instellingen", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Immich heeft toestemming nodig om je foto's en video's te bekijken.", "profile_drawer_app_logs": "Logboek", "profile_drawer_client_server_up_to_date": "App en server zijn up-to-date", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Instellingen", "profile_drawer_sign_out": "Uitloggen", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Album aanmaken mislukt", "select_user_for_sharing_page_share_suggestions": "Suggesties", "server_info_box_app_version": "Appversie", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "Serverversie", "setting_image_viewer_help": "De gedetailleerde weergave laadt eerst de kleine thumbnail, vervolgens het middelgrote voorbeeld (indien ingeschakeld) en ten slotte het origineel (indien ingeschakeld).", "setting_image_viewer_original_subtitle": "Schakel in om de originele afbeelding met volledige resolutie (groot!) te laden. Schakel uit om datagebruik te verminderen (zowel netwerk als apparaatcache).", @@ -300,6 +315,12 @@ "share_add_photos": "Foto's toevoegen", "share_add_title": "Titel toevoegen", "share_create_album": "Album aanmaken", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Voorbereiden...", "shared_link_app_bar_title": "Shared Links", "shared_link_create_app_bar_title": "Create link to share", @@ -311,6 +332,9 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", diff --git a/mobile/assets/i18n/pl-PL.json b/mobile/assets/i18n/pl-PL.json index e9a8e305f..0c7cebe24 100644 --- a/mobile/assets/i18n/pl-PL.json +++ b/mobile/assets/i18n/pl-PL.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Nie udało się zmienić tytułu albumu", "album_viewer_appbar_share_leave": "Opuść album", "album_viewer_appbar_share_remove": "Usuń z albumu", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Dodaj użytkowników", "all_people_page_title": "Ludzie", "all_videos_page_title": "Filmy", + "app_bar_signout_dialog_content": "Czy na pewno chcesz się wylogować?", + "app_bar_signout_dialog_ok": "Tak", + "app_bar_signout_dialog_title": "Wyloguj się", "archive_page_no_archived_assets": "Nie znaleziono zarchiwizowanych zasobów", "archive_page_title": "Archiwum ({})", "asset_list_layout_settings_dynamic_layout_title": "Układ dynamiczny", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Użycie Cache", "cache_settings_subtitle": "Kontrolowanie zachowania buforowania aplikacji mobilnej Immich", "cache_settings_thumbnail_size": "Rozmiar pamięci podręcznej miniatur ({} zasobów)", + "cache_settings_tile_subtitle": "Kontroluj zachowanie lokalnego magazynu", + "cache_settings_tile_title": "Lokalny magazyn", "cache_settings_title": "Ustawienia Buforowania", "change_password_form_confirm_password": "Potwierdź Hasło", - "change_password_form_description": "Cześć {firstName} {lastName},\n\nPierwszy raz logujesz się do systemu, albo złożono prośbę o zmianę hasła. Wpisz poniżej nowe hasło.", + "change_password_form_description": "Cześć {name},\n\nPierwszy raz logujesz się do systemu, albo złożono prośbę o zmianę hasła. Wpisz poniżej nowe hasło.", "change_password_form_new_password": "Nowe Hasło", "change_password_form_password_mismatch": "Hasła nie są zgodne", "change_password_form_reenter_new_password": "Wprowadź ponownie Nowe Hasło", @@ -131,9 +137,9 @@ "control_bottom_app_bar_favorite": "Ulubione", "control_bottom_app_bar_share": "Udostępnij", "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_stack": "Stos", "control_bottom_app_bar_unarchive": "Cofnij archiwizację", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "Wgraj", "create_album_page_untitled": "Bez tytułu", "create_shared_album_page_create": "Utwórz", "create_shared_album_page_share": "Udostępnij", @@ -148,8 +154,8 @@ "delete_dialog_cancel": "Anuluj", "delete_dialog_ok": "Usuń", "delete_dialog_title": "Usuń trwale", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_content": "Czy na pewno chcesz usunąć ten udostępniony link?", + "delete_shared_link_dialog_title": "Usuń udostępniony link", "description_input_hint_text": "Dodaj opis...", "description_input_submit_error": "Błąd aktualizacji opisu, sprawdź dziennik, aby uzyskać więcej szczegółów", "exif_bottom_sheet_description": "Dodaj Opis...", @@ -164,14 +170,19 @@ "home_page_add_to_album_conflicts": "Dodano {added} zasoby do albumu {album}. {failed} zasobów jest już w albumie.", "home_page_add_to_album_err_local": "Nie można dodawać zasobów lokalnych do albumów, pomijam", "home_page_add_to_album_success": "Dodano {added} zasoby do albumu {album}.", + "home_page_album_err_partner": "Nie można jeszcze dodać zasobów partnera do albumu, pomijam", "home_page_archive_err_local": "Nie można jeszcze zarchiwizować zasobów lokalnych, pomijanie", + "home_page_archive_err_partner": "Nie można zarchiwizować zasobów partnera, pomijam", "home_page_building_timeline": "Budowanie osi czasu", + "home_page_delete_err_partner": "Nie można usunąć zasobów partnera, pomijam", "home_page_favorite_err_local": "Nie można dodać do ulubionych lokalnych zasobów, pomijam", + "home_page_favorite_err_partner": "Nie można jeszcze dodać do ulubionych zasobów partnera, pomijam", "home_page_first_time_notice": "Jeśli korzystasz z aplikacji po raz pierwszy, pamiętaj o wybraniu albumów zapasowych, aby oś czasu mogła zapełnić zdjęcia i filmy w albumach.", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Można przesłać maksymalnie 30 zasobów jednocześnie, pomijanie", "image_viewer_page_state_provider_download_error": "Błąd pobierania", "image_viewer_page_state_provider_download_success": "Pobieranie zakończone", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "Udostępnij błąd", "library_page_albums": "Albumy", "library_page_archive": "Archiwum", "library_page_device_albums": "Albumy na Urządzeniu", @@ -179,8 +190,8 @@ "library_page_new_album": "Nowy album", "library_page_sharing": "Udostępnianie", "library_page_sort_created": "Ostatnio utworzone", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "Ostatnio zmodyfikowany", + "library_page_sort_most_recent_photo": "Najnowsze zdjęcie", "library_page_sort_title": "Tytuł albumu", "login_disabled": "Logowanie zostało wyłączone", "login_form_api_exception": "Wyjątek API. Sprawdź adres URL serwera i spróbuj ponownie.", @@ -218,7 +229,7 @@ "map_settings_dialog_cancel": "Anuluj", "map_settings_dialog_save": "Zapisz", "map_settings_dialog_title": "Ustawienia mapy", - "map_settings_include_show_archived": "Include Archived", + "map_settings_include_show_archived": "Uwzględnij zarchiwizowane", "map_settings_only_relative_range": "Zakres dat", "map_settings_only_show_favorites": "Pokaż tylko ulubione", "map_zoom_to_see_photos": "Pomniejsz, aby zobaczyć zdjęcia", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} nie będziesz już mieć dostępu do swoich zdjęć.", "partner_page_stop_sharing_title": "Przestać udostępniać swoje zdjęcia?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Kontynuuj mimo to", "permission_onboarding_get_started": "Rozpocznij", "permission_onboarding_go_to_settings": "Przejdź do ustawień", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich potrzebuje pozwolenia na przeglądanie Twoich zdjęć i filmów.", "profile_drawer_app_logs": "Logi", "profile_drawer_client_server_up_to_date": "Klient i serwer są aktualne", + "profile_drawer_documentation": "Dokumentacja", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Ustawienia", "profile_drawer_sign_out": "Wyloguj się", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "Kosz", "recently_added_page_title": "Ostatnio Dodane", "search_bar_hint": "Szukaj swoich zdjęć", "search_page_categories": "Kategorie", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Nie udało się utworzyć albumu", "select_user_for_sharing_page_share_suggestions": "Propozycje", "server_info_box_app_version": "Wersja Aplikacji", + "server_info_box_server_url": "Adres URL", "server_info_box_server_version": "Wersja Serwera", "setting_image_viewer_help": "Przeglądarka szczegółów najpierw ładuje małą miniaturę, następnie ładuje podgląd średniej wielkości (jeśli jest włączony), a na koniec ładuje oryginał (jeśli jest włączony).", "setting_image_viewer_original_subtitle": "Włącz ładowanie oryginalnego obrazu w pełnej rozdzielczości (dużego!). Wyłącz, aby zmniejszyć zużycie danych (zarówno w sieci, jak i w pamięci podręcznej urządzenia).", @@ -300,28 +315,37 @@ "share_add_photos": "Dodaj zdjęcia", "share_add_title": "Dodaj tytuł", "share_create_album": "Utwórz album", + "shared_album_activities_input_disable": "Komentarz jest wyłączony", + "shared_album_activities_input_hint": "Powiedz coś", + "shared_album_activity_remove_content": "Czy chcesz usunąć tę aktywność?", + "shared_album_activity_remove_title": "Usuń aktywność", + "shared_album_activity_setting_subtitle": "Pozwól innym odpowiedzieć", + "shared_album_activity_setting_title": "Komentarze i polubienia", "share_dialog_preparing": "Przygotowywanie...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_app_bar_title": "Udostępnione linki", + "shared_link_create_app_bar_title": "Utwórz link do udostępnienia", + "shared_link_create_info": "Pozwól każdemu, kto ma link, zobaczyć wybrane zdjęcia", + "shared_link_create_submit_button": "Utwórz link", + "shared_link_edit_allow_download": "Zezwalaj użytkownikowi publicznemu na pobieranie", + "shared_link_edit_allow_upload": "Zezwalaj użytkownikowi publicznemu na przesyłanie", + "shared_link_edit_app_bar_title": "Edytuj link", + "shared_link_edit_change_expiry": "Zmień czas ważności", + "shared_link_edit_description": "Opis", + "shared_link_edit_description_hint": "Wprowadź opis udostępnienia", + "shared_link_edit_expire_after": "Wygasa po", + "shared_link_edit_password": "Hasło", + "shared_link_edit_password_hint": "Wprowadź hasło udostępniania", + "shared_link_edit_show_meta": "Pokaż metadane", + "shared_link_edit_submit_button": "Aktualizuj link", + "shared_link_empty": "Nie masz żadnych udostępnionych linków", + "shared_link_manage_links": "Zarządzaj udostępnionymi linkami", + "share_done": "Zrobione", "share_invite": "Zaproś do albumu", "sharing_page_album": "Udostępnione albumy", "sharing_page_description": "Twórz wspóldzielone albumy, aby udostępniać zdjęcia i filmy osobom w sieci.", "sharing_page_empty_list": "PUSTA LISTA", "sharing_silver_appbar_create_shared_album": "Utwórz współdzielony album", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "Udostępnione linki", "sharing_silver_appbar_share_partner": "Udostępnij partnerce/partnerowi", "tab_controller_nav_library": "Biblioteka", "tab_controller_nav_photos": "Zdjęcia", @@ -338,18 +362,18 @@ "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", "translated_text_options": "Opcje", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", + "trash_page_delete": "Usuń", + "trash_page_delete_all": "Usuń wszystko", + "trash_page_empty_trash_btn": "Opróżnij kosz", + "trash_page_empty_trash_dialog_content": "Czy chcesz opróżnić swoje usunięte zasoby? Przedmioty te zostaną trwale usunięte z Immich", "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", + "trash_page_info": "Elementy przeniesione do kosza zostaną trwale usunięte po {} dniach", + "trash_page_no_assets": "Brak usuniętych zasobów", + "trash_page_restore": "Przywrócić", + "trash_page_restore_all": "Przywrócić wszystkie", + "trash_page_select_assets_btn": "Wybierz zasoby", + "trash_page_select_btn": "Wybierz", + "trash_page_title": "Kosz({})", "upload_dialog_cancel": "Anuluj", "upload_dialog_info": "Czy chcesz wykonać kopię zapasową wybranych zasobów na serwerze?", "upload_dialog_ok": "Prześlij", @@ -360,7 +384,7 @@ "version_announcement_overlay_text_2": "prosimy o poświęcenie czasu na odwiedzenie ", "version_announcement_overlay_text_3": " i upewnij się, że twoja konfiguracja docker-compose i .env jest aktualna, aby zapobiec błędnym konfiguracjom, zwłaszcza jeśli używasz WatchTower lub dowolnego mechanizmu, który obsługuje automatyczną aktualizację aplikacji serwera.", "version_announcement_overlay_title": "Nowa wersja serwera dostępna \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "Usuń ze stosu", + "viewer_stack_use_as_main_asset": "Użyj jako głównego zasobu", + "viewer_unstack": "Usuń stos" } \ No newline at end of file diff --git a/mobile/assets/i18n/ru-RU.json b/mobile/assets/i18n/ru-RU.json index 542eab218..2767a8200 100644 --- a/mobile/assets/i18n/ru-RU.json +++ b/mobile/assets/i18n/ru-RU.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Ошибка переименования альбома", "album_viewer_appbar_share_leave": "Покинуть альбом", "album_viewer_appbar_share_remove": "Удалить из альбома", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Добавить пользователей", "all_people_page_title": "Люди", "all_videos_page_title": "Видео", + "app_bar_signout_dialog_content": "Вы уверены, что хотите выйти из системы?", + "app_bar_signout_dialog_ok": "Да", + "app_bar_signout_dialog_title": "Выйти из системы", "archive_page_no_archived_assets": "В архиве сейчас пусто", "archive_page_title": "Архив ({})", "asset_list_layout_settings_dynamic_layout_title": "Динамическое расположение", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Размер кэша", "cache_settings_subtitle": "Управление кэшированием мобильного приложения Immich", "cache_settings_thumbnail_size": "Размер кэша эскизов ({} объектов)", + "cache_settings_tile_subtitle": "Управление поведением локального хранилища", + "cache_settings_tile_title": "Локальное хранилище", "cache_settings_title": "Настройки кэширования", "change_password_form_confirm_password": "Подтвердите пароль", - "change_password_form_description": "Привет {firstName} {lastName},\n\nЭто либо ваш первый вход в систему, либо был сделан запрос на смену пароля. Пожалуйста, введите новый пароль ниже.", + "change_password_form_description": "Привет {name},\n\nЭто либо ваш первый вход в систему, либо был сделан запрос на смену пароля. Пожалуйста, введите новый пароль ниже.", "change_password_form_new_password": "Новый пароль", "change_password_form_password_mismatch": "Пароли не совпадают", "change_password_form_reenter_new_password": "Повторно введите новый пароль", @@ -130,10 +136,10 @@ "control_bottom_app_bar_delete": "Удалить", "control_bottom_app_bar_favorite": "Избранное", "control_bottom_app_bar_share": "Поделиться", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_share_to": "Поделиться", + "control_bottom_app_bar_stack": "Стек", "control_bottom_app_bar_unarchive": "Восстановить", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "Загрузить", "create_album_page_untitled": "Без названия", "create_shared_album_page_create": "Создать", "create_shared_album_page_share": "Поделиться", @@ -148,8 +154,8 @@ "delete_dialog_cancel": "Отменить", "delete_dialog_ok": "Удалить", "delete_dialog_title": "Удалить навсегда", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_content": "Вы уверены, что хотите удалить эту общую ссылку?", + "delete_shared_link_dialog_title": "Удалить общую ссылку", "description_input_hint_text": "Добавить описание...", "description_input_submit_error": "Не удалось обновить описание, проверьте логи, чтобы узнать причину", "exif_bottom_sheet_description": "Добавить описание...", @@ -164,14 +170,19 @@ "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": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Пока невозможно добавить локальные объекты в архив, пропускаем", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Построение временной шкалы", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Пока не удается добавить в избранное локальные объекты, пропускаем", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Если вы используете приложение впервые, убедитесь, что вы выбрали резервный(е) альбом(ы), чтобы временная шкала могла заполнить фотографии и видео в альбоме(ах).", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Вы можете выгрузить максимум 30 файлов за раз", "image_viewer_page_state_provider_download_error": "Ошибка загрузки", "image_viewer_page_state_provider_download_success": "Успешно загружено", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "Ошибка при публикации", "library_page_albums": "Альбомы", "library_page_archive": "Архив", "library_page_device_albums": "Альбомы на устройстве", @@ -179,8 +190,8 @@ "library_page_new_album": "Новый альбом", "library_page_sharing": "Общие", "library_page_sort_created": "По новизне", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "Последнее изменение", + "library_page_sort_most_recent_photo": "Последняя фотография", "library_page_sort_title": "По названию альбома", "login_disabled": "Вход отключен", "login_form_api_exception": "Ошибка при попытке взаимодействия с сервером. Проверьте URL-адрес до него и попробуйте еще раз.", @@ -218,7 +229,7 @@ "map_settings_dialog_cancel": "Отмена", "map_settings_dialog_save": "Сохранить", "map_settings_dialog_title": "Настройки карты", - "map_settings_include_show_archived": "Include Archived", + "map_settings_include_show_archived": "Включить архивные данные", "map_settings_only_relative_range": "Период времени", "map_settings_only_show_favorites": "Показать только избранное", "map_zoom_to_see_photos": "Уменьшение масштаба для просмотра фотографий", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} больше не сможет получить доступ к вашим фотографиям", "partner_page_stop_sharing_title": "Закрыть доступ партнёра к вашим фото?", "partner_page_title": "Партнёр", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Все равно продолжить", "permission_onboarding_get_started": "Давайте начнём", "permission_onboarding_go_to_settings": "Перейти в настройки", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich просит вас предоставить разрешение на доступ к вашим фото и видео", "profile_drawer_app_logs": "Журнал", "profile_drawer_client_server_up_to_date": "Клиент и сервер обновлены", + "profile_drawer_documentation": "Документация", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Настройки", "profile_drawer_sign_out": "Выйти", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "Корзина", "recently_added_page_title": "Недавно добавленные", "search_bar_hint": "Поиск фотографий", "search_page_categories": "Категории", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "\nНе удалось создать альбом", "select_user_for_sharing_page_share_suggestions": "Предложения", "server_info_box_app_version": "Версия приложения", + "server_info_box_server_url": "URL сервера", "server_info_box_server_version": "Версия сервера", "setting_image_viewer_help": "Средство просмотра деталей сначала загружает маленькую миниатюру, затем загружает предварительный просмотр среднего размера (если включено) и, наконец, загружает оригинал (если включено).", "setting_image_viewer_original_subtitle": "Включите загрузку оригинального изображения в полном разрешении (большое!). Отключите, чтобы уменьшить объем данных (как в сети, так и в кеше устройства).", @@ -300,28 +315,37 @@ "share_add_photos": "Добавить фото", "share_add_title": "Добавить название", "share_create_album": "Создать альбом", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Подготовка...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_app_bar_title": "Общие ссылки", + "shared_link_create_app_bar_title": "Создать ссылку для совместного использования", + "shared_link_create_info": "Позволить любому человеку, имеющему ссылку, увидеть выбранную фотографию (фотографии)", + "shared_link_create_submit_button": "Создать ссылку", + "shared_link_edit_allow_download": "Разрешить публичному пользователю скачивать", + "shared_link_edit_allow_upload": "Разрешить публичному пользователю загружать файлы", + "shared_link_edit_app_bar_title": "Редактировать ссылку", + "shared_link_edit_change_expiry": "Изменить срок действия доступа", + "shared_link_edit_description": "Описание", + "shared_link_edit_description_hint": "Введите описание совместного доступа", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Пароль", + "shared_link_edit_password_hint": "Введите пароль общего доступа", + "shared_link_edit_show_meta": "Показать метаданные", + "shared_link_edit_submit_button": "Обновить ссылку", + "shared_link_empty": "У вас нет общих ссылок", + "shared_link_manage_links": "Управление общими ссылками", + "share_done": "Выполнено", "share_invite": "\nПригласить в альбом", "sharing_page_album": "Общие альбомы", "sharing_page_description": "Создавайте общие альбомы, чтобы делиться фотографиями и видео с людьми в вашей сети.", "sharing_page_empty_list": "ПУСТОЙ СПИСОК", "sharing_silver_appbar_create_shared_album": "Создать общий альбом", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "Общие ссылки", "sharing_silver_appbar_share_partner": "Поделиться с партнёром", "tab_controller_nav_library": "Библиотека", "tab_controller_nav_photos": "Фото", @@ -338,18 +362,18 @@ "theme_setting_three_stage_loading_subtitle": "Трехэтапная загрузка может повысить производительность загрузки, но вызывает значительно более высокую нагрузку на сеть", "theme_setting_three_stage_loading_title": "Включить трехэтапную загрузку", "translated_text_options": "Опции", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", + "trash_page_delete": "Удалить", + "trash_page_delete_all": "Удалить все", + "trash_page_empty_trash_btn": "Очистить корзину", + "trash_page_empty_trash_dialog_content": "Вы хотите очистить свою корзину? Эти объекты будут навсегда удалены из Immich", + "trash_page_empty_trash_dialog_ok": "ОК", + "trash_page_info": "Удаленные элементы будут окончательно удалены через {} дней", + "trash_page_no_assets": "Отсутствие удаленных объектов", + "trash_page_restore": "Восстановить", + "trash_page_restore_all": "Восстановить все", + "trash_page_select_assets_btn": "Выбранные объекты", + "trash_page_select_btn": "Выбрать", + "trash_page_title": "Корзина ({})", "upload_dialog_cancel": "Отмена", "upload_dialog_info": "Вы хотите загрузить выбранный объект(ы) на ваш сервер?", "upload_dialog_ok": "Загрузить", @@ -360,7 +384,7 @@ "version_announcement_overlay_text_2": "пожалуйста, найдите время, чтобы посетить", "version_announcement_overlay_text_3": " и убедитесь, что ваши настройки docker-compose и .env обновлены, чтобы предотвратить любые неправильные настройки, особенно если вы используете WatchTower или любой другой механизм, который обрабатывает обновление вашего серверного приложения автоматически.", "version_announcement_overlay_title": "Доступна новая версия сервера \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "Удалить из стека", + "viewer_stack_use_as_main_asset": "Использование в качестве основного объекта", + "viewer_unstack": "Разобрать стек" } \ No newline at end of file diff --git a/mobile/assets/i18n/sk-SK.json b/mobile/assets/i18n/sk-SK.json index 42ff34680..3e4921ffe 100644 --- a/mobile/assets/i18n/sk-SK.json +++ b/mobile/assets/i18n/sk-SK.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Nepodarilo sa zmeniť názov albumu", "album_viewer_appbar_share_leave": "Opustiť album", "album_viewer_appbar_share_remove": "Odstrániť z albumu", + "album_viewer_appbar_share_to": "Zdieľať s", "album_viewer_page_share_add_users": "Pridať používateľov", "all_people_page_title": "Ľudia", "all_videos_page_title": "Videá", + "app_bar_signout_dialog_content": "Skutočne sa chcete odhlásiť?", + "app_bar_signout_dialog_ok": "Áno", + "app_bar_signout_dialog_title": "Odhlásiť sa", "archive_page_no_archived_assets": "Žiadne archivované médiá", "archive_page_title": "Archív ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamické rozloženie", @@ -65,7 +69,7 @@ "backup_controller_page_background_is_on": "Automatické zálohovanie na pozadí je zapnuté", "backup_controller_page_background_turn_off": "Vypnúť zálohovanie na pozadí", "backup_controller_page_background_turn_on": "Povoliť zálohovanie na pozadí", - "backup_controller_page_background_wifi": "Len na WiFi", + "backup_controller_page_background_wifi": "Len cez WiFi", "backup_controller_page_backup": "Zálohovanie", "backup_controller_page_backup_selected": "Vybrané: ", "backup_controller_page_backup_sub": "Zálohované fotografie a videa", @@ -86,7 +90,7 @@ "backup_controller_page_status_off": "Automatické zálohovanie na popredí je vypnuté", "backup_controller_page_status_on": "Automatické zálohovanie na popredí je zapnuté", "backup_controller_page_storage_format": "{} z {} použitých", - "backup_controller_page_to_backup": "Albumy, ktoré sa majú zálohovať", + "backup_controller_page_to_backup": "Albumy ktoré budú zálohované", "backup_controller_page_total": "Celkom", "backup_controller_page_total_sub": "Všetky jedinečné fotografie a videá z vybraných albumov", "backup_controller_page_turn_off": "Vypnúť zálohovanie na popredí", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Použitie vyrovnávacej pamäte", "cache_settings_subtitle": "Ovládanie správania mobilnej aplikácie Immich v medzipamäti", "cache_settings_thumbnail_size": "Veľkosť vyrovnávacej pamäte náhľadov (položiek {})", + "cache_settings_tile_subtitle": "Ovládanie správania lokálneho úložiska", + "cache_settings_tile_title": "Lokálne úložisko", "cache_settings_title": "Nastavenia vyrovnávacej pamäte", "change_password_form_confirm_password": "Potvrďte heslo", - "change_password_form_description": "Dobrý deň, {firstName} {lastName},\n\nBuď sa do systému prihlasujete prvýkrát, alebo bola podaná žiadosť o zmenu hesla. Prosím, zadajte nové heslo nižšie.", + "change_password_form_description": "Dobrý deň, {name},\n\nBuď sa do systému prihlasujete prvýkrát, alebo bola podaná žiadosť o zmenu hesla. Prosím, zadajte nové heslo nižšie.", "change_password_form_new_password": "Nové heslo", "change_password_form_password_mismatch": "Heslá sa nezhodujú", "change_password_form_reenter_new_password": "Znova zadajte nové heslo", @@ -125,15 +131,15 @@ "control_bottom_app_bar_add_to_album": "Pridať do albumu", "control_bottom_app_bar_album_info": "{} položiek", "control_bottom_app_bar_album_info_shared": "{} položiek - zdieľané", - "control_bottom_app_bar_archive": "Archív", + "control_bottom_app_bar_archive": "Archivovať", "control_bottom_app_bar_create_new_album": "Vytvoriť nový album", "control_bottom_app_bar_delete": "Vymazať", "control_bottom_app_bar_favorite": "Obľúbené", "control_bottom_app_bar_share": "Zdieľať", "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", + "control_bottom_app_bar_stack": "Zoskupenie", "control_bottom_app_bar_unarchive": "Odarchivovať", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "Nahrať", "create_album_page_untitled": "Bez názvu", "create_shared_album_page_create": "Vytvoriť", "create_shared_album_page_share": "Zdieľať", @@ -148,8 +154,8 @@ "delete_dialog_cancel": "Zrušiť", "delete_dialog_ok": "Vymazať", "delete_dialog_title": "Vymazať natrvalo", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_content": "Ste si istí že chcete odstrániť tento zdieľaný odkaz?", + "delete_shared_link_dialog_title": "Odstrániť zdieľaný odkaz", "description_input_hint_text": "Pridať popis...", "description_input_submit_error": "Chyba pri aktualizovaní popisu, zobrazte log pre viac detailov", "exif_bottom_sheet_description": "Pridať popis...", @@ -164,14 +170,19 @@ "home_page_add_to_album_conflicts": "Pridané {added} položiek do albumu {album}. {failed} položiek už je v albume.", "home_page_add_to_album_err_local": "Zatiaľ nie je možné pridať lokálne média do albumov, preskakuje sa", "home_page_add_to_album_success": "Pridané {added} položky do albumu {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Zatiaľ nemožno archivovať lokálne médiá, preskakuje sa", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Vytváranie časovej osi", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Zatiaľ nie je možné zaradiť lokálne média medzi obľúbené, preskakuje sa", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Ak aplikáciu používate prvý krát, nezabudnite si vybrať zálohované albumy, aby sa na časovej osi mohli nachádzať fotografie a videá z vybraných albumoch.", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Naraz môžete nahrať len 30 médií, preskakujem...", "image_viewer_page_state_provider_download_error": "Chyba sťahovania", "image_viewer_page_state_provider_download_success": "Sťahovanie bolo úspešné", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "Chyba zdieľania", "library_page_albums": "Albumy", "library_page_archive": "Archív", "library_page_device_albums": "Albumy v zariadení", @@ -179,8 +190,8 @@ "library_page_new_album": "Nový album", "library_page_sharing": "Zdieľanie", "library_page_sort_created": "Najnovšie vytvorené", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "Naposledy upravené", + "library_page_sort_most_recent_photo": "Najnovšia fotka", "library_page_sort_title": "Podľa názvu albumu", "login_disabled": "Prihlasovanie bolo vypnuté", "login_form_api_exception": "Chyba API. Skontrolujte adresu URL servera a skúste to znova.", @@ -218,7 +229,7 @@ "map_settings_dialog_cancel": "Zrušiť", "map_settings_dialog_save": "Uložiť", "map_settings_dialog_title": "Nastavenia máp", - "map_settings_include_show_archived": "Include Archived", + "map_settings_include_show_archived": "Zahrnúť archivované", "map_settings_only_relative_range": "Rozsah dátumu", "map_settings_only_show_favorites": "Zobraziť iba obľúbené", "map_zoom_to_see_photos": "Oddiaľte priblíženie aby ste videli fotky", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} už nebude mať prístup ku vašim fotkám.", "partner_page_stop_sharing_title": "Zastaviť zdieľanie vašich fotiek?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Pokračovať aj tak", "permission_onboarding_get_started": "Začať", "permission_onboarding_go_to_settings": "Prejsť do nastavení", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich vyžaduje povolenie na prezeranie vašich fotografií a videí.", "profile_drawer_app_logs": "Logy", "profile_drawer_client_server_up_to_date": "Klient a server sú aktuálne", + "profile_drawer_documentation": "Dokumentácia", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Nastavenia", "profile_drawer_sign_out": "Odhlásiť sa", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "Kôš", "recently_added_page_title": "Nedávno pridané", "search_bar_hint": "Prehľadajte svoje obrázky", "search_page_categories": "Kategórie", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Nepodarilo sa vytvoriť album", "select_user_for_sharing_page_share_suggestions": "Návrhy", "server_info_box_app_version": "Verzia aplikácie", + "server_info_box_server_url": "URL Serveru", "server_info_box_server_version": "Verzia servera", "setting_image_viewer_help": "Prehliadač detailov najprv načíta malú miniatúru, potom načíta náhľad strednej veľkosti (ak je povolený) a nakoniec načíta originál (ak je povolený).", "setting_image_viewer_original_subtitle": "Povolením umožníte načítať pôvodný obrázok v plnom rozlíšení (veľký!). Zakázaním znížite používania dát (v sieti, aj v dočasnej pamäte zariadenia).", @@ -300,28 +315,37 @@ "share_add_photos": "Pridať fotografie", "share_add_title": "Pridať názov", "share_create_album": "Vytvoriť album", + "shared_album_activities_input_disable": "Komentár je zakázaný", + "shared_album_activities_input_hint": "Napíšte niečo", + "shared_album_activity_remove_content": "Chcete vymazať túto aktivitu?", + "shared_album_activity_remove_title": "Vymazať aktivitu", + "shared_album_activity_setting_subtitle": "Nechajte ostatných reagovať", + "shared_album_activity_setting_title": "Komentáre a lajky", "share_dialog_preparing": "Pripravujem...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_app_bar_title": "Zdieľané odkazy", + "shared_link_create_app_bar_title": "Vytvoriť odkaz na zdieľanie", + "shared_link_create_info": "Umožniť komukoľvek s odkazom zobraziť označené médiá", + "shared_link_create_submit_button": "Vytvoriť odkaz", + "shared_link_edit_allow_download": "Povoliť návštevníkom sťahovať médiá", + "shared_link_edit_allow_upload": "Povoliť návštevníkom pridávať médiá", + "shared_link_edit_app_bar_title": "Upraviť odkaz", + "shared_link_edit_change_expiry": "Zmeniť čas vypršania", + "shared_link_edit_description": "Popis", + "shared_link_edit_description_hint": "Zadajte popis zdieľania", + "shared_link_edit_expire_after": "Expiruje po", + "shared_link_edit_password": "Heslo", + "shared_link_edit_password_hint": "Zadajte heslo zdieľania", + "shared_link_edit_show_meta": "Zobraziť metadáta", + "shared_link_edit_submit_button": "Aktualizovať odkaz", + "shared_link_empty": "Zatiaľ nemáte žiadne zdieľané odkazy", + "shared_link_manage_links": "Spravovať zdieľané odkazy", + "share_done": "Hotovo", "share_invite": "Pozvať do albumu", "sharing_page_album": "Zdieľané albumy", "sharing_page_description": "Vytvárajte zdieľané albumy a zdieľajte fotografie a videá s ľuďmi vo vašej sieti.", "sharing_page_empty_list": "Prázdny list", "sharing_silver_appbar_create_shared_album": "Vytvoriť zdieľaný album", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "Zdieľané odkazy", "sharing_silver_appbar_share_partner": "Zdieľať s partnerom", "tab_controller_nav_library": "Knižnica", "tab_controller_nav_photos": "Fotografie", @@ -338,18 +362,18 @@ "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", "translated_text_options": "Nastavenia", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", + "trash_page_delete": "Vymazať", + "trash_page_delete_all": "Vymazať všetky", + "trash_page_empty_trash_btn": "Vyprázdniť kôš", + "trash_page_empty_trash_dialog_content": "Skutočne chcete vyprázdniť kôš? Tieto položky budú permanentne odstránené z Immichu", "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", + "trash_page_info": "Médiá v koši sa permanentne odstránia po {} dňoch", + "trash_page_no_assets": "Žiadne médiá v koši", + "trash_page_restore": "Obnoviť", + "trash_page_restore_all": "Obnoviť všetky", + "trash_page_select_assets_btn": "Označiť médiá", + "trash_page_select_btn": "Označiť", + "trash_page_title": "Kôš ({})", "upload_dialog_cancel": "Zrušiť", "upload_dialog_info": "Chcete zálohovať zvolené médiá na server?", "upload_dialog_ok": "Nahrať", @@ -360,7 +384,7 @@ "version_announcement_overlay_text_2": "nájdite si čas na návštevu ", "version_announcement_overlay_text_3": " a uistite sa, že vaša konfigurácia docker-compose a .env je aktuálna, aby ste predišli nesprávnej konfigurácii, najmä ak používate WatchTower alebo akýkoľvek mechanizmus, ktorý podporuje automatické aktualizácie serverových aplikácií.", "version_announcement_overlay_title": "K dispozícii je nová verzia servera \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "Odstrániť zo zoskupenia", + "viewer_stack_use_as_main_asset": "Použiť ako hlavnú fotku", + "viewer_unstack": "Odskupiť" } \ No newline at end of file diff --git a/mobile/assets/i18n/sr-Cyrl.json b/mobile/assets/i18n/sr-Cyrl.json index 31311535b..9c42afb7d 100644 --- a/mobile/assets/i18n/sr-Cyrl.json +++ b/mobile/assets/i18n/sr-Cyrl.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Failed to change album title", "album_viewer_appbar_share_leave": "Leave album", "album_viewer_appbar_share_remove": "Remove from album", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Add users", "all_people_page_title": "People", "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", "archive_page_title": "Archive ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Cache usage", "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", @@ -164,10 +170,15 @@ "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", "home_page_add_to_album_success": "Added {added} assets to album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Can not archive local assets yet, skipping", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Building the timeline", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "image_viewer_page_state_provider_download_error": "Download Error", "image_viewer_page_state_provider_download_success": "Download Success", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Get started", "permission_onboarding_go_to_settings": "Go to settings", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "profile_drawer_app_logs": "Logs", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Settings", "profile_drawer_sign_out": "Sign Out", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Failed to create album", "select_user_for_sharing_page_share_suggestions": "Suggestions", "server_info_box_app_version": "App Version", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "Server Version", "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", @@ -300,6 +315,12 @@ "share_add_photos": "Add photos", "share_add_title": "Add a title", "share_create_album": "Create album", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Preparing...", "shared_link_app_bar_title": "Shared Links", "shared_link_create_app_bar_title": "Create link to share", @@ -311,6 +332,9 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", diff --git a/mobile/assets/i18n/sr-Latn.json b/mobile/assets/i18n/sr-Latn.json index 6273f33fc..9588a2ede 100644 --- a/mobile/assets/i18n/sr-Latn.json +++ b/mobile/assets/i18n/sr-Latn.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Neuspešno menjanje naziva albuma", "album_viewer_appbar_share_leave": "Izađi iz albuma", "album_viewer_appbar_share_remove": "Obriši iz albuma", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Dodaj korisnike", "all_people_page_title": "People", "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", "archive_page_title": "Archive ({})", "asset_list_layout_settings_dynamic_layout_title": "Dinamični raspored", @@ -111,6 +115,8 @@ "cache_settings_statistics_title": "Iskorišćena keš memorija", "cache_settings_subtitle": "Kontrole za keš memoriju mobilne aplikacije Immich", "cache_settings_thumbnail_size": "Keš memorija koju zauzimaju minijature ({} stavki)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Opcije za keširanje", "change_password_form_confirm_password": "Ponovo unesite šifru", "change_password_form_description": "Ćao, {firstName}, {lastName}\n\nOvo je verovatno Vaše prvo pristupanje sistemu, ili je podnešen zahtev za promenu šifre. Molimo Vas, unesite novu šifru ispod", @@ -164,10 +170,15 @@ "home_page_add_to_album_conflicts": "Dodat {added} zapis u album {album}. {failed} zapisi su već u albumu ", "home_page_add_to_album_err_local": "Trenutno nemoguće dodati lokalne zapise u albume, preskacu se", "home_page_add_to_album_success": "Dodate {added} stavke u album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Can not archive local assets yet, skipping", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Kreiranje hronološke linije", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Trenutno nije moguce dodati lokalne zapise u favorite, preskacu se", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Ako je ovo prvi put da koristite aplikaciju, molimo Vas da odaberete albume koje želite da sačuvate", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "image_viewer_page_state_provider_download_error": "Preuzimanje Neuspešno", "image_viewer_page_state_provider_download_success": "Preuzimanje Uspešno", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Get started", "permission_onboarding_go_to_settings": "Go to settings", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "profile_drawer_app_logs": "Evidencija", "profile_drawer_client_server_up_to_date": "Klijent i server su najnovije verzije", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Opcije", "profile_drawer_sign_out": "Odjavi se", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Neuspešno kreiranje albuma", "select_user_for_sharing_page_share_suggestions": "Sugestije", "server_info_box_app_version": "Verzija Aplikacije", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "Verzija Servera", "setting_image_viewer_help": "Detaljno pregledanje prvo učitava minijaturu, pa srednju, pa original. (Ako te opcije uključene)", "setting_image_viewer_original_subtitle": "Aktiviraj učitavanje slika u punoj rezoluciji (Velika!). Deaktivacijom ove stavke možeš da smanjiš potrošnju interneta i zauzetog prostora na uređaju.", @@ -300,6 +315,12 @@ "share_add_photos": "Dodaj fotografije", "share_add_title": "Dodaj naslov", "share_create_album": "Napravi album", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Pripremanje...", "shared_link_app_bar_title": "Shared Links", "shared_link_create_app_bar_title": "Create link to share", @@ -311,6 +332,9 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", diff --git a/mobile/assets/i18n/sv-FI.json b/mobile/assets/i18n/sv-FI.json index 31311535b..9c42afb7d 100644 --- a/mobile/assets/i18n/sv-FI.json +++ b/mobile/assets/i18n/sv-FI.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Failed to change album title", "album_viewer_appbar_share_leave": "Leave album", "album_viewer_appbar_share_remove": "Remove from album", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Add users", "all_people_page_title": "People", "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", "archive_page_title": "Archive ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Cache usage", "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", @@ -164,10 +170,15 @@ "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", "home_page_add_to_album_success": "Added {added} assets to album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Can not archive local assets yet, skipping", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Building the timeline", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "image_viewer_page_state_provider_download_error": "Download Error", "image_viewer_page_state_provider_download_success": "Download Success", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Get started", "permission_onboarding_go_to_settings": "Go to settings", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "profile_drawer_app_logs": "Logs", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Settings", "profile_drawer_sign_out": "Sign Out", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Failed to create album", "select_user_for_sharing_page_share_suggestions": "Suggestions", "server_info_box_app_version": "App Version", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "Server Version", "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", @@ -300,6 +315,12 @@ "share_add_photos": "Add photos", "share_add_title": "Add a title", "share_create_album": "Create album", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Preparing...", "shared_link_app_bar_title": "Shared Links", "shared_link_create_app_bar_title": "Create link to share", @@ -311,6 +332,9 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", diff --git a/mobile/assets/i18n/sv-SE.json b/mobile/assets/i18n/sv-SE.json index 04ae04992..026219fe5 100644 --- a/mobile/assets/i18n/sv-SE.json +++ b/mobile/assets/i18n/sv-SE.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Kunde inte ändra albumtitel", "album_viewer_appbar_share_leave": "Lämna album", "album_viewer_appbar_share_remove": "Ta bort från album", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Lägg till användare", "all_people_page_title": "People", "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", "archive_page_title": "Arkivera ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Cacheförbrukning", "cache_settings_subtitle": "Hantera cachebeteendet för Immich-appen.", "cache_settings_thumbnail_size": "Storlek på cacheminnet ({} bilder och videor)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Cache Inställningar", "change_password_form_confirm_password": "Bekräfta lösenord", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "Nytt lösenord", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", @@ -164,10 +170,15 @@ "home_page_add_to_album_conflicts": "Lade till {added} foton och videor i albumet {album}. {failed} foton och videor finns redan i albumet.", "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", "home_page_add_to_album_success": "Lade till {added} foton och videor i albumet {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Can not archive local assets yet, skipping", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Bygger tidslinjen", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Om det här är första gången du använder appen, välj ett eller flera backup-album så att tidslinjen kan fyllas med foton och videor från albumen.", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "image_viewer_page_state_provider_download_error": "Download Error", "image_viewer_page_state_provider_download_success": "Download Success", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Sluta dela dina foton?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Kom igång", "permission_onboarding_go_to_settings": "Gå till inställningar", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Immich kräver tillstånd för att se dina foton och videor.", "profile_drawer_app_logs": "Loggar", "profile_drawer_client_server_up_to_date": "Klient och server är uppdaterade", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Inställningar", "profile_drawer_sign_out": "Logga ut", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Kunde inte skapa nytt album", "select_user_for_sharing_page_share_suggestions": "Förslag", "server_info_box_app_version": "App version", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "Server version", "setting_image_viewer_help": "Detaljerad vy laddar miniatyrer först. Efter detta laddas den medelstora förhandsgranskningen av bilden (om detta är aktiverat), och visar slutligen originalet (om detta är aktiverat).", "setting_image_viewer_original_subtitle": "Aktivera för att ladda originalbilden i full storlek (stor!). Inaktivera för att minska dataanvändningen (både i nätverket och för enhetscache).", @@ -300,6 +315,12 @@ "share_add_photos": "Lägg till foton", "share_add_title": "Lägg till en titel", "share_create_album": "Skapa album", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Förbereder...", "shared_link_app_bar_title": "Shared Links", "shared_link_create_app_bar_title": "Create link to share", @@ -311,6 +332,9 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", diff --git a/mobile/assets/i18n/th-TH.json b/mobile/assets/i18n/th-TH.json index 6f89ce173..0cfad7eff 100644 --- a/mobile/assets/i18n/th-TH.json +++ b/mobile/assets/i18n/th-TH.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "เปลี่ยนชื่ออัลบั้มไม่สำเร็จ", "album_viewer_appbar_share_leave": "ออกจากอัลบั้ม", "album_viewer_appbar_share_remove": "ลบออกจากอัลบั้ม", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "เพิ่มผู้ใช้งาน", "all_people_page_title": "ผู้คน", "all_videos_page_title": "วิดีโอ", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "ไม่พบทรัพยากรในที่เก็บถาวร", "archive_page_title": "เก็บถาวร ({})", "asset_list_layout_settings_dynamic_layout_title": "แผนผังปรับตัว", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "การใช้งานแคช", "cache_settings_subtitle": "ควบคุมพฤติกรรมการแคชของแอปพลิเคชัน Immich", "cache_settings_thumbnail_size": "ขนาดแคชรูปย่อ ({} ทรัพยากร)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "ตั้งค่าแคช", "change_password_form_confirm_password": "ยืนยันรหัสผ่าน", - "change_password_form_description": "สวัสดี {firstName} {lastName},\n\nครั้งนี้อาจจะเป็นครั้งแรกที่คุณเข้าสู่ระบบ หรือมีคำขอเพื่อที่จะเปลี่ยนรหัสผ่านของคุI กรุณาเพิ่มรหัสผ่านใหม่ข้างล่าง", + "change_password_form_description": "สวัสดี {name},\n\nครั้งนี้อาจจะเป็นครั้งแรกที่คุณเข้าสู่ระบบ หรือมีคำขอเพื่อที่จะเปลี่ยนรหัสผ่านของคุI กรุณาเพิ่มรหัสผ่านใหม่ข้างล่าง", "change_password_form_new_password": "รหัสผ่านใหม่", "change_password_form_password_mismatch": "รหัสผ่านไม่ตรงกัน", "change_password_form_reenter_new_password": "กรอกรหัสผ่านใหม่", @@ -164,10 +170,15 @@ "home_page_add_to_album_conflicts": "เพิ่ม {added} ทรัพยากรเข้าอัลบั้ม {album}. {failed} ทรัพยากรอยู่ในอัลบั้มอยู่แล้ว", "home_page_add_to_album_err_local": " ไม่สามารถเพิ่มทรัพยากรบนเครื่องเข้าอัลบั้ม กำลังข้าม", "home_page_add_to_album_success": "Added {added} assets to album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "ไม่สามารถเก็บถาวรในขณะนี้ กำลังข้าม", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "กำลังสร้าง timeline", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": " ไม่สามารถตั้งทรัพยากรบนเครื่องเป็นรายการโปรด กำลังข้าม", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "ถ้าครั้งนี้เป็นครั้งแรกที่ใช้แอปนี้ กรุณาเลือกอัลบั้มที่จะสำรองข้อมูล ไทม์ไลน์จะได้เพิ่มรูปภาพและวิดีโอที่อยู่ในอัลบั้ม", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "สามารถอัพโหลดได้มากสุดครั้งละ 30 ทรัพยากร กำลังข้าม", "image_viewer_page_state_provider_download_error": "ดาวน์โหลดผิดพลาด", "image_viewer_page_state_provider_download_success": "ดาวน์โหลดสำเร็จ", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} จะไม่สามารถเข้าถึงรูปภาพของคุณ", "partner_page_stop_sharing_title": "หยุดแชร์รูปภาพ?", "partner_page_title": "พันธมิตร", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "ดำเนินการต่อ", "permission_onboarding_get_started": "เริ่มต้น", "permission_onboarding_go_to_settings": "ไปยังการตั้งค่า", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Immich จำเป็นจะต้องได้รับสิทธิ์ดูรูปภาพและวิดีโอ", "profile_drawer_app_logs": "Log", "profile_drawer_client_server_up_to_date": "ไคลเอนต์และเซิร์ฟเวอร์เป็นปัจจุบัน", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Settings", "profile_drawer_sign_out": "ออกจากระบบ", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "สร้างอัลบั้มล้มเหลว", "select_user_for_sharing_page_share_suggestions": "Suggestions", "server_info_box_app_version": "เวอร์ชั่นแอพ", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "เวอร์ชั้นเซิร์ฟเวอร์", "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", @@ -300,6 +315,12 @@ "share_add_photos": "เพิ่มรูปภาพ", "share_add_title": "เพิ่มชื่อ", "share_create_album": "สร้างอัลบั้ม", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "กำลังเตรียม...", "shared_link_app_bar_title": "Shared Links", "shared_link_create_app_bar_title": "Create link to share", @@ -311,6 +332,9 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", diff --git a/mobile/assets/i18n/uk-UA.json b/mobile/assets/i18n/uk-UA.json index 6a733c4f1..d3df92692 100644 --- a/mobile/assets/i18n/uk-UA.json +++ b/mobile/assets/i18n/uk-UA.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Не вдалося змінити назву альбому", "album_viewer_appbar_share_leave": "Вийти з альбому", "album_viewer_appbar_share_remove": "Видалити з альбому", + "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Додати користувачів", "all_people_page_title": "Люди", "all_videos_page_title": "Відео", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "app_bar_signout_dialog_ok": "Yes", + "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "Немає архівних елементів", "archive_page_title": "Архів ({})", "asset_list_layout_settings_dynamic_layout_title": "Динамічне компонування", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Використання кешу", "cache_settings_subtitle": "Контролює кешування у мобільному застосунку", "cache_settings_thumbnail_size": "Розмір кешованих мініатюр ({} елементи)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Налаштування Кешування", "change_password_form_confirm_password": "Підтвердити пароль", - "change_password_form_description": "Привіт {firstName} {lastName},\n\nВи або або вперше входите у систему, або було зроблено запит на зміну вашого пароля. \nВведіть ваш новий пароль.", + "change_password_form_description": "Привіт {name},\n\nВи або або вперше входите у систему, або було зроблено запит на зміну вашого пароля. \nВведіть ваш новий пароль.", "change_password_form_new_password": "Новий Пароль", "change_password_form_password_mismatch": "Паролі не співпадають", "change_password_form_reenter_new_password": "Повторіть Новий Пароль", @@ -164,10 +170,15 @@ "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": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Поки що неможливо заархівувати локальні елементи, пропущено", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Побудова хронології", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Неможливо отримати улюблені локальні елементи, пропущено", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Якщо ви вперше користуєтеся програмою, переконайтеся, що ви вибрали альбоми для резервування, щоб могти заповнювати хронологію знімків та відео в альбомах.", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Можна вантажити не більше 30 елементів водночас, пропущено", "image_viewer_page_state_provider_download_error": "Помилка завантаження", "image_viewer_page_state_provider_download_success": "Усіпшно завантажено", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} втратить доступ до ваших знімків.", "partner_page_stop_sharing_title": "Припинити надання ваших знімків?", "partner_page_title": "Партнер", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Все одно продовжити", "permission_onboarding_get_started": "Розпочати", "permission_onboarding_go_to_settings": "Перейти до налаштувань", @@ -250,6 +262,8 @@ "permission_onboarding_request": "Immich потребує доступу до ваших знімків та відео.", "profile_drawer_app_logs": "Журнал", "profile_drawer_client_server_up_to_date": "Клієнт та Сервер — актуальні", + "profile_drawer_documentation": "Documentation", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Налаштування", "profile_drawer_sign_out": "Вийти", "profile_drawer_trash": "Trash", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Не вдалося створити альбом", "select_user_for_sharing_page_share_suggestions": "Suggestions", "server_info_box_app_version": "App Version", + "server_info_box_server_url": "Server URL", "server_info_box_server_version": "Server Version", "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", @@ -300,6 +315,12 @@ "share_add_photos": "Додати знімки", "share_add_title": "Додати назву", "share_create_album": "Створити альбом", + "shared_album_activities_input_disable": "Comment is disabled", + "shared_album_activities_input_hint": "Say something", + "shared_album_activity_remove_content": "Do you want to delete this activity?", + "shared_album_activity_remove_title": "Delete Activity", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "Comments & likes", "share_dialog_preparing": "Підготовка...", "shared_link_app_bar_title": "Shared Links", "shared_link_create_app_bar_title": "Create link to share", @@ -311,6 +332,9 @@ "shared_link_edit_change_expiry": "Change expiration time", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "Password", + "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", diff --git a/mobile/assets/i18n/vi-VN.json b/mobile/assets/i18n/vi-VN.json index 340efef7d..60edb10b4 100644 --- a/mobile/assets/i18n/vi-VN.json +++ b/mobile/assets/i18n/vi-VN.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "Failed to change album title", "album_viewer_appbar_share_leave": "Leave album", "album_viewer_appbar_share_remove": "Remove from album", + "album_viewer_appbar_share_to": "Chia sẻ với", "album_viewer_page_share_add_users": "Add users", - "all_people_page_title": "People", + "all_people_page_title": "Mọi người", "all_videos_page_title": "Videos", + "app_bar_signout_dialog_content": "Bạn có muốn đăng xuất?", + "app_bar_signout_dialog_ok": "Có", + "app_bar_signout_dialog_title": "Đăng xuất", "archive_page_no_archived_assets": "No archived assets found", "archive_page_title": "Archive ({})", "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", @@ -69,7 +73,7 @@ "backup_controller_page_backup": "Backup", "backup_controller_page_backup_selected": "Selected: ", "backup_controller_page_backup_sub": "Backed up photos and videos", - "backup_controller_page_cancel": "Cancel", + "backup_controller_page_cancel": "Từ chối", "backup_controller_page_created": "Created on: {}", "backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.", "backup_controller_page_excluded": "Excluded: ", @@ -95,10 +99,10 @@ "backup_err_only_album": "Cannot remove the only album", "backup_info_card_assets": "assets", "backup_manual_cancelled": "Cancelled", - "backup_manual_failed": "Failed", + "backup_manual_failed": "Thất bại", "backup_manual_in_progress": "Upload already in progress. Try after sometime", - "backup_manual_success": "Success", - "backup_manual_title": "Upload status", + "backup_manual_success": "Thành công", + "backup_manual_title": "Trạng thái tải lên", "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", "cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "Cache usage", "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", + "cache_settings_tile_subtitle": "Kiểm soát cách xử lý lưu trữ cục bộ", + "cache_settings_tile_title": "Lưu trữ cục bộ", "cache_settings_title": "Caching Settings", "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", @@ -130,10 +136,10 @@ "control_bottom_app_bar_delete": "Delete", "control_bottom_app_bar_favorite": "Favorite", "control_bottom_app_bar_share": "Share", - "control_bottom_app_bar_share_to": "Share To", + "control_bottom_app_bar_share_to": "Chia sẻ với", "control_bottom_app_bar_stack": "Stack", "control_bottom_app_bar_unarchive": "Unarchive", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "Tải lên", "create_album_page_untitled": "Untitled", "create_shared_album_page_create": "Create", "create_shared_album_page_share": "Share", @@ -145,11 +151,11 @@ "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device", - "delete_dialog_cancel": "Cancel", + "delete_dialog_cancel": "Từ chối", "delete_dialog_ok": "Delete", "delete_dialog_title": "Delete Permanently", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_content": "Bạn có muốn xóa liên kết đã chia sẻ này không?", + "delete_shared_link_dialog_title": "Xoá liên kết đã chia sẻ", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", "exif_bottom_sheet_description": "Add Description...", @@ -164,10 +170,15 @@ "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", "home_page_add_to_album_success": "Added {added} assets to album {album}.", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Can not archive local assets yet, skipping", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Building the timeline", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "image_viewer_page_state_provider_download_error": "Download Error", "image_viewer_page_state_provider_download_success": "Download Success", @@ -179,8 +190,8 @@ "library_page_new_album": "New album", "library_page_sharing": "Sharing", "library_page_sort_created": "Most recently created", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "Sửa đổi lần cuối", + "library_page_sort_most_recent_photo": "Ảnh gần đây nhất", "library_page_sort_title": "Album title", "login_disabled": "Login has been disabled", "login_form_api_exception": "API exception. Please check the server URL and try again.", @@ -199,32 +210,32 @@ "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", "login_form_label_email": "Email", "login_form_label_password": "Password", - "login_form_next_button": "Next", + "login_form_next_button": "Tiếp tục", "login_form_password_hint": "password", "login_form_save_login": "Stay logged in", "login_form_server_empty": "Enter a server URL.", "login_form_server_error": "Could not connect to server.", "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", + "login_password_changed_success": "Cập nhật mật khẩu thành công", "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_cancel": "Cancel", - "map_location_dialog_yes": "Yes", + "map_location_dialog_cancel": "Từ chối", + "map_location_dialog_yes": "Có", "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", "map_location_service_disabled_title": "Location Service disabled", "map_no_assets_in_bounds": "No photos in this area", "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", "map_no_location_permission_title": "Location Permission denied", - "map_settings_dark_mode": "Dark mode", - "map_settings_dialog_cancel": "Cancel", - "map_settings_dialog_save": "Save", - "map_settings_dialog_title": "Map Settings", + "map_settings_dark_mode": "Chế độ tối", + "map_settings_dialog_cancel": "Từ chối", + "map_settings_dialog_save": "Lưu", + "map_settings_dialog_title": "Cài đặt bản đồ", "map_settings_include_show_archived": "Include Archived", "map_settings_only_relative_range": "Date range", "map_settings_only_show_favorites": "Show Favorite Only", "map_zoom_to_see_photos": "Zoom out to see photos", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Motion Photos", - "notification_permission_dialog_cancel": "Cancel", + "notification_permission_dialog_cancel": "Từ chối", "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", "notification_permission_dialog_settings": "Settings", "notification_permission_list_tile_content": "Grant permission to enable notifications.", @@ -235,10 +246,11 @@ "partner_page_no_more_users": "No more users to add", "partner_page_partner_add_failed": "Failed to add partner", "partner_page_select_partner": "Select partner", - "partner_page_shared_to_title": "Shared to", + "partner_page_shared_to_title": "Chia sẻ với", "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", "partner_page_title": "Partner", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Get started", "permission_onboarding_go_to_settings": "Go to settings", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich requires permission to view your photos and videos.", "profile_drawer_app_logs": "Logs", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", + "profile_drawer_documentation": "Tài liệu", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "Settings", "profile_drawer_sign_out": "Sign Out", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "Thùng rác", "recently_added_page_title": "Recently Added", "search_bar_hint": "Search your photos", "search_page_categories": "Categories", @@ -260,7 +274,7 @@ "search_page_motion_photos": "Motion Photos", "search_page_no_objects": "Không có thông tin vật thể", "search_page_no_places": "No Places Info Available", - "search_page_people": "People", + "search_page_people": "Mọi người", "search_page_places": "Places", "search_page_recently_added": "Recently added", "search_page_screenshots": "Screenshots", @@ -276,6 +290,7 @@ "select_user_for_sharing_page_err_album": "Failed to create album", "select_user_for_sharing_page_share_suggestions": "Gợi ý", "server_info_box_app_version": "App Version", + "server_info_box_server_url": "Địa chỉ máy chủ", "server_info_box_server_version": "Server Version", "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", @@ -300,28 +315,37 @@ "share_add_photos": "Add photos", "share_add_title": "Add a title", "share_create_album": "Create album", + "shared_album_activities_input_disable": "Nhận xét hiện đã tắt", + "shared_album_activities_input_hint": "Nói điều gì đó", + "shared_album_activity_remove_content": "Bạn có muốn xoá hoạt động này?", + "shared_album_activity_remove_title": "Xoá hoạt động", + "shared_album_activity_setting_subtitle": "Cho phép người khác phản hồi", + "shared_album_activity_setting_title": "Bình luận và lượt thích", "share_dialog_preparing": "Preparing...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_app_bar_title": "Đường liên kết chia sẻ", + "shared_link_create_app_bar_title": "Tạo liên kết để chia sẻ", + "shared_link_create_info": "Cho phép bất cứ ai có liên kết xem (các) ảnh đã chọn", + "shared_link_create_submit_button": "Tạo liên kết", + "shared_link_edit_allow_download": "Cho phép bất cứ ai đều có thể tải xuống", + "shared_link_edit_allow_upload": "Cho phép bất cứ ai đều có thể tải lên", + "shared_link_edit_app_bar_title": "Chỉnh sửa liên kết", + "shared_link_edit_change_expiry": "Thay đổi thời gian hết hạn", + "shared_link_edit_description": "Mô tả", + "shared_link_edit_description_hint": "Nhập mô tả chia sẻ", + "shared_link_edit_expire_after": "Hết hạn sau", + "shared_link_edit_password": "Mật khẩu", + "shared_link_edit_password_hint": "Nhập mật khẩu chia sẻ", + "shared_link_edit_show_meta": "Hiện thị siêu dữ liệu", + "shared_link_edit_submit_button": "Cập nhật liên kết", + "shared_link_empty": "Bạn không có liên kết được chia sẻ nào", + "shared_link_manage_links": "Quản lý liên kết được chia sẻ", + "share_done": "Hoàn tất", "share_invite": "Invite to album", "sharing_page_album": "Shared albums", "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", "sharing_page_empty_list": "EMPTY LIST", "sharing_silver_appbar_create_shared_album": "Create shared album", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "Các liên kết chia sẻ", "sharing_silver_appbar_share_partner": "Share with partner", "tab_controller_nav_library": "Library", "tab_controller_nav_photos": "Photos", @@ -337,22 +361,22 @@ "theme_setting_theme_title": "Theme", "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", - "translated_text_options": "Options", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", - "upload_dialog_cancel": "Cancel", + "translated_text_options": "Tuỳ chỉnh", + "trash_page_delete": "Xoá", + "trash_page_delete_all": "Xoá tất cả", + "trash_page_empty_trash_btn": "Dọn sạch thùng rác", + "trash_page_empty_trash_dialog_content": "Bạn có muốn dọn sạch thùng rác của mình không? Những mục này sẽ bị xoá vĩnh viễn khỏi Immich", + "trash_page_empty_trash_dialog_ok": "Đồng ý", + "trash_page_info": "Những mục này sẽ bị xoá sau {} ngày", + "trash_page_no_assets": "Không có ảnh hoặc video", + "trash_page_restore": "Khôi phục", + "trash_page_restore_all": "Khôi phục tất cả", + "trash_page_select_assets_btn": "Chọn mục", + "trash_page_select_btn": "Chọn", + "trash_page_title": "Thùng rác ({})", + "upload_dialog_cancel": "Từ chối", "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", - "upload_dialog_ok": "Upload", + "upload_dialog_ok": "Tải lên", "upload_dialog_title": "Upload Asset", "version_announcement_overlay_ack": "Acknowledge", "version_announcement_overlay_release_notes": "release notes", diff --git a/mobile/assets/i18n/zh-CN.json b/mobile/assets/i18n/zh-CN.json index 6ddf38545..28591e395 100644 --- a/mobile/assets/i18n/zh-CN.json +++ b/mobile/assets/i18n/zh-CN.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "修改相册标题失败", "album_viewer_appbar_share_leave": "退出共享", "album_viewer_appbar_share_remove": "从相册中移除", + "album_viewer_appbar_share_to": "共享给", "album_viewer_page_share_add_users": "创建用户", "all_people_page_title": "人物", "all_videos_page_title": "视频", + "app_bar_signout_dialog_content": "您确定要注消吗?", + "app_bar_signout_dialog_ok": "是", + "app_bar_signout_dialog_title": "注消", "archive_page_no_archived_assets": "未找到归档项目", "archive_page_title": "归档({})", "asset_list_layout_settings_dynamic_layout_title": "动态布局", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "缓存使用情况", "cache_settings_subtitle": "控制 Immich 的缓存行为", "cache_settings_thumbnail_size": "缩略图缓存大小({} 项)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "本地存储", "cache_settings_title": "缓存设置", "change_password_form_confirm_password": "确认密码", - "change_password_form_description": "{firstName} {lastName} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。", + "change_password_form_description": "{name} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。", "change_password_form_new_password": "新密码", "change_password_form_password_mismatch": "密码不匹配", "change_password_form_reenter_new_password": "重新输入新的密码", @@ -130,10 +136,10 @@ "control_bottom_app_bar_delete": "删除", "control_bottom_app_bar_favorite": "收藏", "control_bottom_app_bar_share": "共享", - "control_bottom_app_bar_share_to": "Share To", + "control_bottom_app_bar_share_to": "共享给", "control_bottom_app_bar_stack": "Stack", "control_bottom_app_bar_unarchive": "取消归档", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "上传", "create_album_page_untitled": "未命名", "create_shared_album_page_create": "创建", "create_shared_album_page_share": "共享", @@ -149,7 +155,7 @@ "delete_dialog_ok": "删除", "delete_dialog_title": "永久删除", "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_title": "删除共享链接", "description_input_hint_text": "添加描述...", "description_input_submit_error": "更新描述时出错,请检查日志以获取更多详细信息", "exif_bottom_sheet_description": "添加描述...", @@ -164,14 +170,19 @@ "home_page_add_to_album_conflicts": "已向相册 {album} 中添加 {added} 项。\n其中 {failed} 项在相册中已存在。", "home_page_add_to_album_err_local": "暂不能将本地项目添加到相册中,跳过", "home_page_add_to_album_success": "已向相册 {album} 中添加 {added} 项。", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "暂无法归档本地项目,跳过", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "正在生成时间线", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "暂不能收藏本地项目,跳过", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "如果这是您第一次使用该应用程序,请确保选择一个要备份的本地相册,以便可以在时间线中预览该相册中的照片和视频。", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "一次最多只能上传 30 个项目,跳过", "image_viewer_page_state_provider_download_error": "下载出现错误", "image_viewer_page_state_provider_download_success": "下载成功", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "共享错误", "library_page_albums": "相册", "library_page_archive": "归档", "library_page_device_albums": "设备上的相册", @@ -179,14 +190,14 @@ "library_page_new_album": "新建相册", "library_page_sharing": "共享", "library_page_sort_created": "最近创建的", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "上次修改", + "library_page_sort_most_recent_photo": "最近的项目", "library_page_sort_title": "相册标题", "login_disabled": "登录已被禁用", "login_form_api_exception": "API 异常,请检查服务器地址并重试。", "login_form_button_text": "登录", "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "http(s)://你的服务器地址:端口/api", + "login_form_endpoint_hint": "http(s)://您的服务器地址:端口/api", "login_form_endpoint_url": "服务器链接地址", "login_form_err_http": "请注明 http:// 或 https://", "login_form_err_invalid_email": "无效的电子邮件", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} 将无法再访问您的照片。", "partner_page_stop_sharing_title": "您确定要停止共享您的照片吗?", "partner_page_title": "同伴", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "仍然继续", "permission_onboarding_get_started": "开始使用", "permission_onboarding_go_to_settings": "转到设置", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich 需要权限才能查看您的照片和视频。", "profile_drawer_app_logs": "日志", "profile_drawer_client_server_up_to_date": "客户端和服务端都是最新的", + "profile_drawer_documentation": "文档", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "设置", "profile_drawer_sign_out": "退出登录", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "回收站", "recently_added_page_title": "最近添加", "search_bar_hint": "搜索照片", "search_page_categories": "类别", @@ -271,11 +285,12 @@ "search_page_your_activity": "您的活动", "search_result_page_new_search_hint": "搜索新的", "search_suggestion_list_smart_search_hint_1": "默认情况下启用智能搜索;要搜索元数据,请使用相关语法", - "search_suggestion_list_smart_search_hint_2": "m:你的搜索关键词", + "search_suggestion_list_smart_search_hint_2": "m:您的搜索关键词", "select_additional_user_for_sharing_page_suggestions": "建议", "select_user_for_sharing_page_err_album": "创建相册失败", "select_user_for_sharing_page_share_suggestions": "建议", "server_info_box_app_version": "App 版本", + "server_info_box_server_url": "服务器地址", "server_info_box_server_version": "服务器版本", "setting_image_viewer_help": "详细信息查看器首先加载小缩略图,然后加载中等大小的预览图(若启用),最后加载原始图像。", "setting_image_viewer_original_subtitle": "启用以加载原图,禁用以减少数据使用量(网络和设备缓存)。", @@ -300,28 +315,37 @@ "share_add_photos": "添加项目", "share_add_title": "添加标题", "share_create_album": "创建相册", + "shared_album_activities_input_disable": "评论已禁用", + "shared_album_activities_input_hint": "说些什么", + "shared_album_activity_remove_content": "您确定要删除此活动吗?", + "shared_album_activity_remove_title": "删除活动", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "评论与喜欢", "share_dialog_preparing": "正在准备...", - "shared_link_app_bar_title": "Shared Links", + "shared_link_app_bar_title": "共享链接", "shared_link_create_app_bar_title": "Create link to share", "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", + "shared_link_create_submit_button": "创建链接", "shared_link_edit_allow_download": "Allow public user to download", "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", + "shared_link_edit_app_bar_title": "编辑链接", + "shared_link_edit_change_expiry": "修改过期时间", + "shared_link_edit_description": "描述", "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "密码", + "shared_link_edit_password_hint": "输入共享密码", + "shared_link_edit_show_meta": "显示元数据", + "shared_link_edit_submit_button": "更新链接", "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_manage_links": "管理共享链接", + "share_done": "完成", "share_invite": "邀请相册共享", "sharing_page_album": "共享相册", "sharing_page_description": "创建共享相册以与网络中的人共享照片和视频。", "sharing_page_empty_list": "空", "sharing_silver_appbar_create_shared_album": "创建共享相册", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "共享链接", "sharing_silver_appbar_share_partner": "共享给同伴", "tab_controller_nav_library": "图库", "tab_controller_nav_photos": "照片", @@ -338,17 +362,17 @@ "theme_setting_three_stage_loading_subtitle": "三段式加载可能会提升加载性能,但可能会导致更高的网络负载", "theme_setting_three_stage_loading_title": "启用三段式加载", "translated_text_options": "选项", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", + "trash_page_delete": "删除", + "trash_page_delete_all": "删除全部", + "trash_page_empty_trash_btn": "清空回收站", "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_empty_trash_dialog_ok": "Ok", + "trash_page_empty_trash_dialog_ok": "好的", "trash_page_info": "Trashed items will be permanently deleted after {} days", "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", + "trash_page_restore": "恢复", + "trash_page_restore_all": "恢复全部", + "trash_page_select_assets_btn": "选择项目", + "trash_page_select_btn": "选择", "trash_page_title": "Trash ({})", "upload_dialog_cancel": "取消", "upload_dialog_info": "是否要将所选项目备份到服务器?", diff --git a/mobile/assets/i18n/zh-Hans.json b/mobile/assets/i18n/zh-Hans.json index 59d67bd66..cc8b89a15 100644 --- a/mobile/assets/i18n/zh-Hans.json +++ b/mobile/assets/i18n/zh-Hans.json @@ -23,9 +23,13 @@ "album_viewer_appbar_share_err_title": "修改相册标题失败", "album_viewer_appbar_share_leave": "退出共享", "album_viewer_appbar_share_remove": "从相册中移除", + "album_viewer_appbar_share_to": "共享给", "album_viewer_page_share_add_users": "创建用户", "all_people_page_title": "人物", "all_videos_page_title": "视频", + "app_bar_signout_dialog_content": "您确定要注消吗?", + "app_bar_signout_dialog_ok": "是", + "app_bar_signout_dialog_title": "注消", "archive_page_no_archived_assets": "未找到归档项目", "archive_page_title": "归档({})", "asset_list_layout_settings_dynamic_layout_title": "动态布局", @@ -111,9 +115,11 @@ "cache_settings_statistics_title": "缓存使用情况", "cache_settings_subtitle": "控制 Immich 的缓存行为", "cache_settings_thumbnail_size": "缩略图缓存大小({} 项)", + "cache_settings_tile_subtitle": "Control the local storage behaviour", + "cache_settings_tile_title": "本地存储", "cache_settings_title": "缓存设置", "change_password_form_confirm_password": "确认密码", - "change_password_form_description": "{firstName} {lastName} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。", + "change_password_form_description": "{name} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。", "change_password_form_new_password": "新密码", "change_password_form_password_mismatch": "密码不匹配", "change_password_form_reenter_new_password": "重新输入新的密码", @@ -130,10 +136,10 @@ "control_bottom_app_bar_delete": "删除", "control_bottom_app_bar_favorite": "收藏", "control_bottom_app_bar_share": "共享", - "control_bottom_app_bar_share_to": "Share To", + "control_bottom_app_bar_share_to": "共享给", "control_bottom_app_bar_stack": "Stack", "control_bottom_app_bar_unarchive": "取消归档", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_upload": "上传", "create_album_page_untitled": "未命名", "create_shared_album_page_create": "创建", "create_shared_album_page_share": "共享", @@ -149,7 +155,7 @@ "delete_dialog_ok": "删除", "delete_dialog_title": "永久删除", "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_shared_link_dialog_title": "删除共享链接", "description_input_hint_text": "添加描述...", "description_input_submit_error": "更新描述时出错,请检查日志以获取更多详细信息", "exif_bottom_sheet_description": "添加描述...", @@ -164,14 +170,19 @@ "home_page_add_to_album_conflicts": "已向相册 {album} 中添加 {added} 项。\n其中 {failed} 项在相册中已存在。", "home_page_add_to_album_err_local": "暂不能将本地资项目添加到相册中,跳过", "home_page_add_to_album_success": "已向相册 {album} 中添加 {added} 项。", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "暂无法归档本地项目,跳过", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "正在生成时间线", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "home_page_favorite_err_local": "暂不能收藏本地项目,跳过", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "如果这是您第一次使用该应用程序,请确保选择一个要备份的本地相册,以便可以在时间线中预览该相册中的照片和视频。", + "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "一次最多只能上传 30 个项目,跳过", "image_viewer_page_state_provider_download_error": "下载出现错误", "image_viewer_page_state_provider_download_success": "下载成功", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "共享错误", "library_page_albums": "相册", "library_page_archive": "归档", "library_page_device_albums": "设备上的相册", @@ -179,14 +190,14 @@ "library_page_new_album": "新建相册", "library_page_sharing": "共享", "library_page_sort_created": "最近创建的", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "上次修改", + "library_page_sort_most_recent_photo": "最近的项目", "library_page_sort_title": "相册标题", "login_disabled": "登录已被禁用", "login_form_api_exception": "API 异常,请检查服务器地址并重试。", "login_form_button_text": "登录", "login_form_email_hint": "youremail@email.com", - "login_form_endpoint_hint": "http(s)://你的服务器地址:端口/api", + "login_form_endpoint_hint": "http(s)://您的服务器地址:端口/api", "login_form_endpoint_url": "服务器链接地址", "login_form_err_http": "请注明 http:// 或 https://", "login_form_err_invalid_email": "无效的电子邮件", @@ -239,6 +250,7 @@ "partner_page_stop_sharing_content": "{} 将无法再访问您的照片。", "partner_page_stop_sharing_title": "您确定要停止共享您的照片吗?", "partner_page_title": "同伴", + "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "仍然继续", "permission_onboarding_get_started": "开始使用", "permission_onboarding_go_to_settings": "转到设置", @@ -250,9 +262,11 @@ "permission_onboarding_request": "Immich 需要权限才能查看您的照片和视频。", "profile_drawer_app_logs": "日志", "profile_drawer_client_server_up_to_date": "客户端和服务端都是最新的", + "profile_drawer_documentation": "文档", + "profile_drawer_github": "GitHub", "profile_drawer_settings": "设置", "profile_drawer_sign_out": "退出登录", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "回收站", "recently_added_page_title": "最近添加", "search_bar_hint": "搜索照片", "search_page_categories": "类别", @@ -271,11 +285,12 @@ "search_page_your_activity": "您的活动", "search_result_page_new_search_hint": "搜索新的", "search_suggestion_list_smart_search_hint_1": "默认情况下启用智能搜索;要搜索元数据,请使用相关语法", - "search_suggestion_list_smart_search_hint_2": "m:你的搜索关键词", + "search_suggestion_list_smart_search_hint_2": "m:您的搜索关键词", "select_additional_user_for_sharing_page_suggestions": "建议", "select_user_for_sharing_page_err_album": "创建相册失败", "select_user_for_sharing_page_share_suggestions": "建议", "server_info_box_app_version": "App 版本", + "server_info_box_server_url": "服务器地址", "server_info_box_server_version": "服务器版本", "setting_image_viewer_help": "详细信息查看器首先加载小缩略图,然后加载中等大小的预览图(若启用),最后加载原始图像。", "setting_image_viewer_original_subtitle": "启用以加载原图,禁用以减少数据使用量(网络和设备缓存)。", @@ -300,28 +315,37 @@ "share_add_photos": "添加项目", "share_add_title": "添加标题", "share_create_album": "创建相册", + "shared_album_activities_input_disable": "评论已禁用", + "shared_album_activities_input_hint": "说些什么", + "shared_album_activity_remove_content": "您确定要删除此活动吗?", + "shared_album_activity_remove_title": "删除活动", + "shared_album_activity_setting_subtitle": "Let others respond", + "shared_album_activity_setting_title": "评论与喜欢", "share_dialog_preparing": "正在准备...", - "shared_link_app_bar_title": "Shared Links", + "shared_link_app_bar_title": "共享链接", "shared_link_create_app_bar_title": "Create link to share", "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", + "shared_link_create_submit_button": "创建链接", "shared_link_edit_allow_download": "Allow public user to download", "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", + "shared_link_edit_app_bar_title": "编辑链接", + "shared_link_edit_change_expiry": "修改过期时间", + "shared_link_edit_description": "描述", "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", + "shared_link_edit_expire_after": "Expire after", + "shared_link_edit_password": "密码", + "shared_link_edit_password_hint": "输入共享密码", + "shared_link_edit_show_meta": "显示元数据", + "shared_link_edit_submit_button": "更新链接", "shared_link_empty": "You don't have any shared links", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_manage_links": "管理共享链接", + "share_done": "完成", "share_invite": "邀请相册共享", "sharing_page_album": "共享相册", "sharing_page_description": "创建共享相册以与网络中的人共享照片和视频。", "sharing_page_empty_list": "空", "sharing_silver_appbar_create_shared_album": "创建共享相册", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "共享链接", "sharing_silver_appbar_share_partner": "共享给同伴", "tab_controller_nav_library": "图库", "tab_controller_nav_photos": "照片", @@ -338,17 +362,17 @@ "theme_setting_three_stage_loading_subtitle": "三段式加载可能会提升加载性能,但可能会导致更高的网络负载", "theme_setting_three_stage_loading_title": "启用三段式加载", "translated_text_options": "选项", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", + "trash_page_delete": "删除", + "trash_page_delete_all": "删除全部", + "trash_page_empty_trash_btn": "清空回收站", "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_empty_trash_dialog_ok": "Ok", + "trash_page_empty_trash_dialog_ok": "好的", "trash_page_info": "Trashed items will be permanently deleted after {} days", "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", + "trash_page_restore": "恢复", + "trash_page_restore_all": "恢复全部", + "trash_page_select_assets_btn": "选择项目", + "trash_page_select_btn": "选择", "trash_page_title": "Trash ({})", "upload_dialog_cancel": "取消", "upload_dialog_info": "是否要将所选项目备份到服务器?", diff --git a/mobile/fonts/WorkSans-Black.ttf b/mobile/fonts/WorkSans-Black.ttf deleted file mode 100644 index f0f61fbde..000000000 Binary files a/mobile/fonts/WorkSans-Black.ttf and /dev/null differ diff --git a/mobile/fonts/WorkSans-Bold.ttf b/mobile/fonts/WorkSans-Bold.ttf deleted file mode 100644 index c30cb0708..000000000 Binary files a/mobile/fonts/WorkSans-Bold.ttf and /dev/null differ diff --git a/mobile/fonts/WorkSans-ExtraBold.ttf b/mobile/fonts/WorkSans-ExtraBold.ttf deleted file mode 100644 index 2d0d46a3a..000000000 Binary files a/mobile/fonts/WorkSans-ExtraBold.ttf and /dev/null differ diff --git a/mobile/fonts/WorkSans-Italic.ttf b/mobile/fonts/WorkSans-Italic.ttf deleted file mode 100644 index f2bf33102..000000000 Binary files a/mobile/fonts/WorkSans-Italic.ttf and /dev/null differ diff --git a/mobile/fonts/WorkSans-Medium.ttf b/mobile/fonts/WorkSans-Medium.ttf deleted file mode 100644 index 1800fe2d8..000000000 Binary files a/mobile/fonts/WorkSans-Medium.ttf and /dev/null differ diff --git a/mobile/fonts/WorkSans-SemiBold.ttf b/mobile/fonts/WorkSans-SemiBold.ttf deleted file mode 100644 index bce808c82..000000000 Binary files a/mobile/fonts/WorkSans-SemiBold.ttf and /dev/null differ diff --git a/mobile/fonts/WorkSans.ttf b/mobile/fonts/WorkSans.ttf deleted file mode 100644 index 09829a516..000000000 Binary files a/mobile/fonts/WorkSans.ttf and /dev/null differ diff --git a/mobile/fonts/overpass/Overpass-Bold.ttf b/mobile/fonts/overpass/Overpass-Bold.ttf new file mode 100644 index 000000000..0cd2fb107 Binary files /dev/null and b/mobile/fonts/overpass/Overpass-Bold.ttf differ diff --git a/mobile/fonts/overpass/Overpass-Italic.ttf b/mobile/fonts/overpass/Overpass-Italic.ttf new file mode 100644 index 000000000..1031a9e57 Binary files /dev/null and b/mobile/fonts/overpass/Overpass-Italic.ttf differ diff --git a/mobile/fonts/overpass/Overpass-Medium.ttf b/mobile/fonts/overpass/Overpass-Medium.ttf new file mode 100644 index 000000000..815276fe2 Binary files /dev/null and b/mobile/fonts/overpass/Overpass-Medium.ttf differ diff --git a/mobile/fonts/overpass/Overpass-Regular.ttf b/mobile/fonts/overpass/Overpass-Regular.ttf new file mode 100644 index 000000000..b89906141 Binary files /dev/null and b/mobile/fonts/overpass/Overpass-Regular.ttf differ diff --git a/mobile/fonts/overpass/Overpass-SemiBold.ttf b/mobile/fonts/overpass/Overpass-SemiBold.ttf new file mode 100644 index 000000000..b8faf9b99 Binary files /dev/null and b/mobile/fonts/overpass/Overpass-SemiBold.ttf differ diff --git a/mobile/fonts/overpass/OverpassMono.ttf b/mobile/fonts/overpass/OverpassMono.ttf new file mode 100644 index 000000000..4502d6123 Binary files /dev/null and b/mobile/fonts/overpass/OverpassMono.ttf differ diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index ac0b14ef4..8daa08d70 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/store.dart'; +import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:integration_test/integration_test.dart'; import 'package:isar/isar.dart'; // ignore: depend_on_referenced_packages @@ -40,7 +42,12 @@ class ImmichTestHelper { await Store.clear(); await db.writeTxn(() => db.clear()); // Load main Widget - await tester.pumpWidget(app.getMainWidget(db)); + await tester.pumpWidget( + ProviderScope( + overrides: [dbProvider.overrideWithValue(db)], + child: const app.MainWidget(), + ), + ); // Post run tasks await EasyLocalization.ensureInitialized(); } diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index c6c23d942..75168ce1c 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -169,4 +169,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382 -COCOAPODS: 1.12.1 +COCOAPODS: 1.11.3 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index b15f480e2..c5c6ed57c 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -379,7 +379,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -515,7 +515,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -543,7 +543,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 124; + CURRENT_PROJECT_VERSION = 128; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index a6fddc98a..2d3c9d1ee 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -43,15 +43,10 @@ mn ko sr - sr hi - es - es - sv ca hu lv - zh th CFBundleName @@ -59,11 +54,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.84.0 + 1.88.0 CFBundleSignature ???? CFBundleVersion - 124 + 128 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 19cefc12f..85b006ab4 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.84.0" + version_number: "1.89.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml index c61f1d5d2..947c79674 100644 --- a/mobile/ios/fastlane/report.xml +++ b/mobile/ios/fastlane/report.xml @@ -5,32 +5,32 @@ - + - + - + - + - + - + diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/constants/immich_colors.dart index e3f601358..598f95661 100644 --- a/mobile/lib/constants/immich_colors.dart +++ b/mobile/lib/constants/immich_colors.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -Color immichBackgroundColor = const Color(0xFFf6f8fe); -Color immichDarkBackgroundColor = const Color.fromARGB(255, 0, 0, 0); -Color immichDarkThemePrimaryColor = const Color.fromARGB(255, 173, 203, 250); +const Color immichBackgroundColor = Color(0xFFf6f8fe); +const Color immichDarkBackgroundColor = Color.fromARGB(255, 0, 0, 0); +const Color immichDarkThemePrimaryColor = Color.fromARGB(255, 173, 203, 250); diff --git a/mobile/lib/constants/locales.dart b/mobile/lib/constants/locales.dart index 452203d51..0913b1978 100644 --- a/mobile/lib/constants/locales.dart +++ b/mobile/lib/constants/locales.dart @@ -9,6 +9,7 @@ const List locales = [ Locale('it', 'IT'), Locale('es', 'ES'), Locale('vi', 'VN'), + Locale('fr', 'CA'), Locale('fr', 'FR'), Locale('ja', 'JP'), Locale('pl', 'PL'), @@ -29,6 +30,7 @@ const List locales = [ Locale('hi', 'IN'), Locale('es', 'PE'), Locale('es', 'MX'), + Locale('es', 'US'), Locale('sv', 'FI'), Locale('ca', 'CA'), Locale('hu', 'HU'), diff --git a/mobile/lib/extensions/asyncvalue_extensions.dart b/mobile/lib/extensions/asyncvalue_extensions.dart new file mode 100644 index 000000000..036881f3c --- /dev/null +++ b/mobile/lib/extensions/asyncvalue_extensions.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; +import 'package:immich_mobile/shared/ui/scaffold_error_body.dart'; +import 'package:logging/logging.dart'; + +extension LogOnError on AsyncValue { + static final Logger _asyncErrorLogger = Logger("AsyncValue"); + + Widget widgetWhen({ + bool skipLoadingOnRefresh = true, + Widget Function()? onLoading, + Widget Function(Object? error, StackTrace? stack)? onError, + required Widget Function(T data) onData, + }) { + if (isLoading) { + bool skip = false; + if (isRefreshing) { + skip = skipLoadingOnRefresh; + } + + if (!skip) { + return onLoading?.call() ?? + const Center( + child: ImmichLoadingIndicator(), + ); + } + } + + if (hasError && !hasValue) { + _asyncErrorLogger.severe("Error occured", error, stackTrace); + return onError?.call(error, stackTrace) ?? const ScaffoldErrorBody(); + } + + return onData(requireValue); + } +} diff --git a/mobile/lib/extensions/build_context_extensions.dart b/mobile/lib/extensions/build_context_extensions.dart new file mode 100644 index 000000000..6151bd1a5 --- /dev/null +++ b/mobile/lib/extensions/build_context_extensions.dart @@ -0,0 +1,54 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; + +extension ContextHelper on BuildContext { + // Returns the current size from MediaQuery + Size get size => MediaQuery.sizeOf(this); + + // Returns the current width from MediaQuery + double get width => size.width; + + // Returns the current height from MediaQuery + double get height => size.height; + + // Returns true if the app is running on a mobile device (!tablets) + bool get isMobile => width < 550; + + // Returns the current ThemeData + ThemeData get themeData => Theme.of(this); + + // Returns true if the app is using a dark theme + bool get isDarkTheme => themeData.brightness == Brightness.dark; + + // Returns the current Primary color of the Theme + Color get primaryColor => themeData.primaryColor; + + // Returns the Scaffold background color of the Theme + Color get scaffoldBackgroundColor => themeData.scaffoldBackgroundColor; + + // Returns the current TextTheme + TextTheme get textTheme => themeData.textTheme; + + // Current ColorScheme used + ColorScheme get colorScheme => themeData.colorScheme; + + // Pop-out from the current context with optional result + void pop([T? result]) => Navigator.of(this).pop(result); + + // Auto-Push new route from the current context + Future autoPush(PageRouteInfo route) => + AutoRouter.of(this).push(route); + + // Auto-Push navigate route from the current context + Future autoNavigate( + PageRouteInfo route, + ) => + AutoRouter.of(this).navigate(route); + + // Auto-Push replace route from the current context + Future autoReplace(PageRouteInfo route) => + AutoRouter.of(this).replace(route); + + // Auto-Pop from the current context + Future autoPop([T? result]) => AutoRouter.of(this).pop(result); +} diff --git a/mobile/lib/utils/builtin_extensions.dart b/mobile/lib/extensions/collection_extensions.dart similarity index 70% rename from mobile/lib/utils/builtin_extensions.dart rename to mobile/lib/extensions/collection_extensions.dart index 5b769f26f..283726ede 100644 --- a/mobile/lib/utils/builtin_extensions.dart +++ b/mobile/lib/extensions/collection_extensions.dart @@ -2,27 +2,6 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; -extension DurationExtension on String { - Duration? toDuration() { - try { - final parts = split(':') - .map((e) => double.parse(e).toInt()) - .toList(growable: false); - return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]); - } catch (e) { - return null; - } - } - - double toDouble() { - return double.parse(this); - } - - int toInt() { - return int.parse(this); - } -} - extension ListExtension on List { List uniqueConsecutive({ int Function(E a, E b)? compare, diff --git a/mobile/lib/utils/datetime_extensions.dart b/mobile/lib/extensions/datetime_extensions.dart similarity index 100% rename from mobile/lib/utils/datetime_extensions.dart rename to mobile/lib/extensions/datetime_extensions.dart diff --git a/mobile/lib/utils/flutter_map_extensions.dart b/mobile/lib/extensions/flutter_map_extensions.dart similarity index 100% rename from mobile/lib/utils/flutter_map_extensions.dart rename to mobile/lib/extensions/flutter_map_extensions.dart diff --git a/mobile/lib/extensions/string_extensions.dart b/mobile/lib/extensions/string_extensions.dart new file mode 100644 index 000000000..a25ab4f50 --- /dev/null +++ b/mobile/lib/extensions/string_extensions.dart @@ -0,0 +1,30 @@ +extension StringExtension on String { + String capitalize() { + return split(" ") + .map( + (str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1), + ) + .join(" "); + } +} + +extension DurationExtension on String { + Duration? toDuration() { + try { + final parts = split(':') + .map((e) => double.parse(e).toInt()) + .toList(growable: false); + return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]); + } catch (e) { + return null; + } + } + + double toDouble() { + return double.parse(this); + } + + int toInt() { + return int.parse(this); + } +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 13eda9d6e..a12c43b6c 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; @@ -7,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:timezone/data/latest.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/modules/backup/background_service/background.service.dart'; @@ -26,11 +28,8 @@ import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/providers/app_state.provider.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; -import 'package:immich_mobile/shared/providers/release_info.provider.dart'; import 'package:immich_mobile/shared/services/immich_logger.service.dart'; import 'package:immich_mobile/shared/services/local_notification.service.dart'; -import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; -import 'package:immich_mobile/shared/views/version_announcement_overlay.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; import 'package:immich_mobile/utils/immich_app_theme.dart'; import 'package:immich_mobile/utils/migration.dart'; @@ -45,7 +44,13 @@ void main() async { await initApp(); await migrateDatabaseIfNeeded(db); HttpOverrides.global = HttpSSLCertOverride(); - runApp(getMainWidget(db)); + + runApp( + ProviderScope( + overrides: [dbProvider.overrideWithValue(db)], + child: const MainWidget(), + ), + ); } Future initApp() async { @@ -105,19 +110,6 @@ Future loadDb() async { return db; } -Widget getMainWidget(Isar db) { - return EasyLocalization( - supportedLocales: locales, - path: translationsPath, - useFallbackTranslations: true, - fallbackLocale: locales.first, - child: ProviderScope( - overrides: [dbProvider.overrideWithValue(db)], - child: const ImmichApp(), - ), - ); -} - class ImmichApp extends ConsumerStatefulWidget { const ImmichApp({super.key}); @@ -167,10 +159,9 @@ class ImmichAppState extends ConsumerState // Android 8 does not support transparent app bars final info = await DeviceInfoPlugin().androidInfo; if (info.version.sdkInt <= 26) { - overlayStyle = - MediaQuery.of(context).platformBrightness == Brightness.light - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark; + overlayStyle = context.isDarkTheme + ? SystemUiOverlayStyle.dark + : SystemUiOverlayStyle.light; } } SystemChrome.setSystemUIOverlayStyle(overlayStyle); @@ -196,30 +187,39 @@ class ImmichAppState extends ConsumerState @override Widget build(BuildContext context) { var router = ref.watch(appRouterProvider); - ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo(); return MaterialApp( localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, locale: context.locale, debugShowCheckedModeBanner: false, - home: Stack( - children: [ - MaterialApp.router( - title: 'Immich', - debugShowCheckedModeBanner: false, - themeMode: ref.watch(immichThemeProvider), - darkTheme: immichDarkTheme, - theme: immichLightTheme, - routeInformationParser: router.defaultRouteParser(), - routerDelegate: router.delegate( - navigatorObservers: () => [TabNavigationObserver(ref: ref)], - ), - ), - const ImmichLoadingOverlay(), - const VersionAnnouncementOverlay(), - ], + home: MaterialApp.router( + title: 'Immich', + debugShowCheckedModeBanner: false, + themeMode: ref.watch(immichThemeProvider), + darkTheme: immichDarkTheme, + theme: immichLightTheme, + routeInformationParser: router.defaultRouteParser(), + routerDelegate: router.delegate( + navigatorObservers: () => [TabNavigationObserver(ref: ref)], + ), ), ); } } + +// ignore: prefer-single-widget-per-file +class MainWidget extends StatelessWidget { + const MainWidget({super.key}); + + @override + Widget build(BuildContext context) { + return EasyLocalization( + supportedLocales: locales, + path: translationsPath, + useFallbackTranslations: true, + fallbackLocale: locales.first, + child: const ImmichApp(), + ); + } +} diff --git a/mobile/lib/modules/activities/models/activity.model.dart b/mobile/lib/modules/activities/models/activity.model.dart index 417ba4a86..2db626f54 100644 --- a/mobile/lib/modules/activities/models/activity.model.dart +++ b/mobile/lib/modules/activities/models/activity.model.dart @@ -48,8 +48,7 @@ class Activity { : ActivityType.like, user = User( email: dto.user.email, - firstName: dto.user.firstName, - lastName: dto.user.lastName, + name: dto.user.name, profileImagePath: dto.user.profileImagePath, id: dto.user.id, // Placeholder values diff --git a/mobile/lib/modules/activities/views/activities_page.dart b/mobile/lib/modules/activities/views/activities_page.dart index 69afe2e5d..f0c68a349 100644 --- a/mobile/lib/modules/activities/views/activities_page.dart +++ b/mobile/lib/modules/activities/views/activities_page.dart @@ -4,13 +4,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/activities/models/activity.model.dart'; import 'package:immich_mobile/modules/activities/providers/activity.provider.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; -import 'package:immich_mobile/utils/datetime_extensions.dart'; +import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; class ActivitiesPage extends HookConsumerWidget { @@ -19,12 +20,14 @@ class ActivitiesPage extends HookConsumerWidget { final bool withAssetThumbs; final String appBarTitle; final bool isOwner; + final bool isReadOnly; const ActivitiesPage( this.albumId, { this.appBarTitle = "", this.assetId, this.withAssetThumbs = true, this.isOwner = false, + this.isReadOnly = false, super.key, }); @@ -45,13 +48,10 @@ class ActivitiesPage extends HookConsumerWidget { }, [], ); + buildTitleWithTimestamp(Activity activity, {bool leftAlign = true}) { - final textColor = Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black; - final textStyle = Theme.of(context) - .textTheme - .bodyMedium + final textColor = context.isDarkTheme ? Colors.white : Colors.black; + final textStyle = context.textTheme.bodyMedium ?.copyWith(color: textColor.withOpacity(0.6)); return Row( @@ -61,7 +61,7 @@ class ActivitiesPage extends HookConsumerWidget { mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max, children: [ Text( - "${activity.user.firstName} ${activity.user.lastName}", + activity.user.name, style: textStyle, overflow: TextOverflow.ellipsis, ), @@ -88,7 +88,7 @@ class ActivitiesPage extends HookConsumerWidget { width: 40, height: 30, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), + borderRadius: const BorderRadius.all(Radius.circular(4)), image: DecorationImage( image: CachedNetworkImageProvider( getThumbnailUrlForRemoteId( @@ -116,6 +116,7 @@ class ActivitiesPage extends HookConsumerWidget { padding: const EdgeInsets.only(bottom: 10), child: TextField( controller: inputController, + enabled: !isReadOnly, focusNode: inputFocusNode, textInputAction: TextInputAction.send, autofocus: false, @@ -150,7 +151,9 @@ class ActivitiesPage extends HookConsumerWidget { ), ), suffixIconColor: liked ? Colors.red[700] : null, - hintText: 'shared_album_activities_input_hint'.tr(), + hintText: isReadOnly + ? 'shared_album_activities_input_disable'.tr() + : 'shared_album_activities_input_hint'.tr(), hintStyle: TextStyle( fontWeight: FontWeight.normal, fontSize: 14, @@ -228,11 +231,8 @@ class ActivitiesPage extends HookConsumerWidget { return Scaffold( appBar: AppBar(title: Text(appBarTitle)), - body: activities.maybeWhen( - orElse: () { - return const Center(child: ImmichLoadingIndicator()); - }, - data: (data) { + body: activities.widgetWhen( + onData: (data) { final liked = data.firstWhereOrNull( (a) => a.type == ActivityType.like && @@ -240,70 +240,72 @@ class ActivitiesPage extends HookConsumerWidget { a.assetId == assetId, ); - return Stack( - children: [ - ListView.builder( - controller: listViewScrollController, - itemCount: data.length + 1, - itemBuilder: (context, index) { - // Vertical gap after the last element - if (index == data.length) { - return const SizedBox( - height: 80, - ); - } + return SafeArea( + child: Stack( + children: [ + ListView.builder( + controller: listViewScrollController, + itemCount: data.length + 1, + itemBuilder: (context, index) { + // Vertical gap after the last element + if (index == data.length) { + return const SizedBox( + height: 80, + ); + } - final activity = data[index]; - final canDelete = - activity.user.id == currentUser?.id || isOwner; + final activity = data[index]; + final canDelete = + activity.user.id == currentUser?.id || isOwner; - return Padding( - padding: const EdgeInsets.all(5), - child: activity.type == ActivityType.comment - ? getDismissibleWidget( - ListTile( - minVerticalPadding: 15, - leading: UserCircleAvatar(user: activity.user), - title: buildTitleWithTimestamp( - activity, - leftAlign: - withAssetThumbs && activity.assetId != null, - ), - titleAlignment: ListTileTitleAlignment.top, - trailing: buildAssetThumbnail(activity), - subtitle: Text(activity.comment!), - ), - activity, - canDelete, - ) - : getDismissibleWidget( - ListTile( - minVerticalPadding: 15, - leading: Container( - width: 44, - alignment: Alignment.center, - child: Icon( - Icons.favorite_rounded, - color: Colors.red[700], + return Padding( + padding: const EdgeInsets.all(5), + child: activity.type == ActivityType.comment + ? getDismissibleWidget( + ListTile( + minVerticalPadding: 15, + leading: UserCircleAvatar(user: activity.user), + title: buildTitleWithTimestamp( + activity, + leftAlign: withAssetThumbs && + activity.assetId != null, ), + titleAlignment: ListTileTitleAlignment.top, + trailing: buildAssetThumbnail(activity), + subtitle: Text(activity.comment!), ), - title: buildTitleWithTimestamp(activity), - trailing: buildAssetThumbnail(activity), + activity, + canDelete, + ) + : getDismissibleWidget( + ListTile( + minVerticalPadding: 15, + leading: Container( + width: 44, + alignment: Alignment.center, + child: Icon( + Icons.favorite_rounded, + color: Colors.red[700], + ), + ), + title: buildTitleWithTimestamp(activity), + trailing: buildAssetThumbnail(activity), + ), + activity, + canDelete, ), - activity, - canDelete, - ), - ); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: buildTextField(liked?.id), + ); + }, ), - ), - ], + Align( + alignment: Alignment.bottomCenter, + child: Container( + color: context.scaffoldBackgroundColor, + child: buildTextField(liked?.id), + ), + ), + ], + ), ); }, ), diff --git a/mobile/lib/modules/album/providers/album.provider.dart b/mobile/lib/modules/album/providers/album.provider.dart index 24679c517..48a2e8f1f 100644 --- a/mobile/lib/modules/album/providers/album.provider.dart +++ b/mobile/lib/modules/album/providers/album.provider.dart @@ -25,6 +25,8 @@ class AlbumNotifier extends StateNotifier> { _albumService.refreshRemoteAlbums(isShared: false), ]); + Future getDeviceAlbums() => _albumService.refreshDeviceAlbums(); + Future deleteAlbum(Album album) => _albumService.deleteAlbum(album); Future createAlbum( diff --git a/mobile/lib/modules/album/providers/shared_album.provider.dart b/mobile/lib/modules/album/providers/shared_album.provider.dart index 4f36c4633..f8084da00 100644 --- a/mobile/lib/modules/album/providers/shared_album.provider.dart +++ b/mobile/lib/modules/album/providers/shared_album.provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; import 'package:immich_mobile/modules/album/services/album.service.dart'; import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/asset.dart'; @@ -10,7 +11,7 @@ import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:isar/isar.dart'; class SharedAlbumNotifier extends StateNotifier> { - SharedAlbumNotifier(this._albumService, Isar db) : super([]) { + SharedAlbumNotifier(this._albumService, Isar db, this._ref) : super([]) { final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc(); query.findAll().then((value) => state = value); _streamSub = query.watch().listen((data) => state = data); @@ -18,6 +19,7 @@ class SharedAlbumNotifier extends StateNotifier> { final AlbumService _albumService; late final StreamSubscription> _streamSub; + final Ref _ref; Future createSharedAlbum( String albumName, @@ -66,6 +68,17 @@ class SharedAlbumNotifier extends StateNotifier> { return result; } + Future setActivityEnabled(Album album, bool activityEnabled) async { + final result = + await _albumService.setActivityEnabled(album, activityEnabled); + + if (result) { + _ref.invalidate(albumDetailProvider(album.id)); + } + + return result; + } + @override void dispose() { _streamSub.cancel(); @@ -78,5 +91,6 @@ final sharedAlbumProvider = return SharedAlbumNotifier( ref.watch(albumServiceProvider), ref.watch(dbProvider), + ref, ); }); diff --git a/mobile/lib/modules/album/services/album.service.dart b/mobile/lib/modules/album/services/album.service.dart index 4488eca23..1f50a3667 100644 --- a/mobile/lib/modules/album/services/album.service.dart +++ b/mobile/lib/modules/album/services/album.service.dart @@ -67,6 +67,10 @@ class AlbumService { final List selectedIds = await _backupService.selectedAlbumsQuery().idProperty().findAll(); if (selectedIds.isEmpty) { + final numLocal = await _db.albums.where().localIdIsNotNull().count(); + if (numLocal > 0) { + _syncService.removeAllLocalAlbumsAndAssets(); + } return false; } final List onDevice = @@ -284,6 +288,23 @@ class AlbumService { return false; } + Future setActivityEnabled(Album album, bool enabled) async { + try { + final result = await _apiService.albumApi.updateAlbumInfo( + album.remoteId!, + UpdateAlbumDto(isActivityEnabled: enabled), + ); + if (result != null) { + album.activityEnabled = enabled; + await _db.writeTxn(() => _db.albums.put(album)); + return true; + } + } catch (e) { + debugPrint("Error setActivityEnabled ${e.toString()}"); + } + return false; + } + Future deleteAlbum(Album album) async { try { final userId = Store.get(StoreKey.currentUser).isarId; diff --git a/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart b/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart index 257dbdbaa..25747177a 100644 --- a/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart +++ b/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart @@ -1,8 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; @@ -65,7 +65,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget { } ref.invalidate(albumDetailProvider(album.id)); - Navigator.pop(context); + context.pop(); } return Card( @@ -95,20 +95,19 @@ class AddToAlbumBottomSheet extends HookConsumerWidget { children: [ Text( 'common_add_to_album'.tr(), - style: Theme.of(context).textTheme.displayMedium, + style: context.textTheme.displayMedium, ), TextButton.icon( icon: Icon( Icons.add, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), label: Text( 'common_create_new_album'.tr(), - style: - TextStyle(color: Theme.of(context).primaryColor), + style: TextStyle(color: context.primaryColor), ), onPressed: () { - AutoRouter.of(context).push( + context.autoPush( CreateAlbumRoute( isSharedAlbum: false, initialAssets: assets, diff --git a/mobile/lib/modules/album/ui/album_action_outlined_button.dart b/mobile/lib/modules/album/ui/album_action_outlined_button.dart index 928a50794..768be0e3c 100644 --- a/mobile/lib/modules/album/ui/album_action_outlined_button.dart +++ b/mobile/lib/modules/album/ui/album_action_outlined_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; class AlbumActionOutlinedButton extends StatelessWidget { final VoidCallback? onPressed; @@ -14,10 +15,8 @@ class AlbumActionOutlinedButton extends StatelessWidget { @override Widget build(BuildContext context) { - final isDarkTheme = Theme.of(context).brightness == Brightness.dark; - return Padding( - padding: const EdgeInsets.only(right: 8.0), + padding: const EdgeInsets.only(right: 16.0), child: OutlinedButton.icon( style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10), @@ -26,21 +25,21 @@ class AlbumActionOutlinedButton extends StatelessWidget { ), side: BorderSide( width: 1, - color: isDarkTheme + color: context.isDarkTheme ? const Color.fromARGB(255, 63, 63, 63) : const Color.fromARGB(255, 206, 206, 206), ), ), icon: Icon( iconData, - size: 15, - color: Theme.of(context).primaryColor, + size: 18, + color: context.primaryColor, ), label: Text( labelText, - style: Theme.of(context).textTheme.labelSmall?.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.w600, + ), ), onPressed: onPressed, ), diff --git a/mobile/lib/modules/album/ui/album_thumbnail_card.dart b/mobile/lib/modules/album/ui/album_thumbnail_card.dart index f37751e3e..b295deec5 100644 --- a/mobile/lib/modules/album/ui/album_thumbnail_card.dart +++ b/mobile/lib/modules/album/ui/album_thumbnail_card.dart @@ -1,5 +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/shared/models/album.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; @@ -22,7 +23,8 @@ class AlbumThumbnailCard extends StatelessWidget { @override Widget build(BuildContext context) { - var isDarkMode = Theme.of(context).brightness == Brightness.dark; + var isDarkTheme = context.isDarkTheme; + return LayoutBuilder( builder: (context, constraints) { var cardSize = constraints.maxWidth; @@ -32,7 +34,7 @@ class AlbumThumbnailCard extends StatelessWidget { height: cardSize, width: cardSize, decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.grey[200], + color: isDarkTheme ? Colors.grey[800] : Colors.grey[200], ), child: Center( child: Icon( @@ -70,17 +72,13 @@ class AlbumThumbnailCard extends StatelessWidget { .tr(args: ['${album.assetCount}']) : 'album_thumbnail_card_items' .tr(args: ['${album.assetCount}']), - style: TextStyle( - fontFamily: 'WorkSans', - fontSize: 12, - color: isDarkMode ? Colors.white : Colors.black, - ), + style: context.textTheme.bodyMedium, ), if (owner != null) const TextSpan(text: ' · '), if (owner != null) TextSpan( text: owner, - style: Theme.of(context).textTheme.labelSmall, + style: context.textTheme.bodyMedium, ), ], ), @@ -112,11 +110,9 @@ class AlbumThumbnailCard extends StatelessWidget { width: cardSize, child: Text( album.name, - style: TextStyle( - fontWeight: FontWeight.bold, - color: isDarkMode - ? Theme.of(context).primaryColor - : Colors.black, + style: context.textTheme.bodyMedium?.copyWith( + color: context.primaryColor, + fontWeight: FontWeight.w500, ), ), ), diff --git a/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart b/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart index c9237ea27..38208e88c 100644 --- a/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart +++ b/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart @@ -1,7 +1,7 @@ -import 'package:auto_route/auto_route.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/store.dart'; @@ -21,12 +21,11 @@ class AlbumThumbnailListTile extends StatelessWidget { @override Widget build(BuildContext context) { var cardSize = 68.0; - var isDarkMode = Theme.of(context).brightness == Brightness.dark; buildEmptyThumbnail() { return Container( decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[800] : Colors.grey[200], + color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[200], ), child: SizedBox( height: cardSize, @@ -61,7 +60,7 @@ class AlbumThumbnailListTile extends StatelessWidget { behavior: HitTestBehavior.opaque, onTap: onTap ?? () { - AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id)); + context.autoPush(AlbumViewerRoute(albumId: album.id)); }, child: Padding( padding: const EdgeInsets.only(bottom: 12.0), diff --git a/mobile/lib/modules/album/ui/album_title_text_field.dart b/mobile/lib/modules/album/ui/album_title_text_field.dart index 38c1f681a..57b82a80d 100644 --- a/mobile/lib/modules/album/ui/album_title_text_field.dart +++ b/mobile/lib/modules/album/ui/album_title_text_field.dart @@ -1,6 +1,7 @@ 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/modules/album/providers/album_title.provider.dart'; class AlbumTitleTextField extends ConsumerWidget { @@ -19,7 +20,7 @@ class AlbumTitleTextField extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isDarkTheme = Theme.of(context).brightness == Brightness.dark; + final isDarkTheme = context.isDarkTheme; return TextField( onChanged: (v) { @@ -55,7 +56,7 @@ class AlbumTitleTextField extends ConsumerWidget { }, icon: Icon( Icons.cancel_rounded, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), splashRadius: 10, ) diff --git a/mobile/lib/modules/album/ui/album_viewer_appbar.dart b/mobile/lib/modules/album/ui/album_viewer_appbar.dart index 05db82e10..0e2fc74fb 100644 --- a/mobile/lib/modules/album/ui/album_viewer_appbar.dart +++ b/mobile/lib/modules/album/ui/album_viewer_appbar.dart @@ -1,8 +1,8 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/activities/providers/activity.provider.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; @@ -43,6 +43,7 @@ class AlbumViewerAppbar extends HookConsumerWidget Widget build(BuildContext context, WidgetRef ref) { final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText; final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum; + final isProcessing = useProcessingOverlay(); final comments = album.shared ? ref.watch( activityStatisticsStateProvider( @@ -52,18 +53,18 @@ class AlbumViewerAppbar extends HookConsumerWidget : 0; deleteAlbum() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; final bool success; if (album.shared) { success = await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album); - AutoRouter.of(context) - .navigate(const TabControllerRoute(children: [SharingRoute()])); + context + .autoNavigate(const TabControllerRoute(children: [SharingRoute()])); } else { success = await ref.watch(albumProvider.notifier).deleteAlbum(album); - AutoRouter.of(context) - .navigate(const TabControllerRoute(children: [LibraryRoute()])); + context + .autoNavigate(const TabControllerRoute(children: [LibraryRoute()])); } if (!success) { ImmichToast.show( @@ -74,7 +75,7 @@ class AlbumViewerAppbar extends HookConsumerWidget ); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } Future showConfirmationDialog() async { @@ -89,27 +90,25 @@ class AlbumViewerAppbar extends HookConsumerWidget ), actions: [ TextButton( - onPressed: () => Navigator.pop(context, 'Cancel'), + onPressed: () => context.pop('Cancel'), child: Text( 'Cancel', style: TextStyle( - color: Theme.of(context).primaryColor, + color: context.primaryColor, fontWeight: FontWeight.bold, ), ), ), TextButton( onPressed: () { - Navigator.pop(context, 'Confirm'); + context.pop('Confirm'); deleteAlbum(); }, child: Text( 'Confirm', style: TextStyle( fontWeight: FontWeight.bold, - color: Theme.of(context).brightness == Brightness.light - ? Colors.red - : Colors.red[300], + color: !context.isDarkTheme ? Colors.red : Colors.red[300], ), ), ), @@ -124,16 +123,16 @@ class AlbumViewerAppbar extends HookConsumerWidget } void onLeaveAlbumPressed() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album); if (isSuccess) { - AutoRouter.of(context) - .navigate(const TabControllerRoute(children: [SharingRoute()])); + context + .autoNavigate(const TabControllerRoute(children: [SharingRoute()])); } else { - Navigator.pop(context); + context.pop(); ImmichToast.show( context: context, msg: "album_viewer_appbar_share_err_leave".tr(), @@ -142,11 +141,11 @@ class AlbumViewerAppbar extends HookConsumerWidget ); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } void onRemoveFromAlbumPressed() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum( @@ -155,12 +154,12 @@ class AlbumViewerAppbar extends HookConsumerWidget ); if (isSuccess) { - Navigator.pop(context); + context.pop(); selectionDisabled(); ref.watch(albumProvider.notifier).getAllAlbums(); ref.invalidate(albumDetailProvider(album.id)); } else { - Navigator.pop(context); + context.pop(); ImmichToast.show( context: context, msg: "album_viewer_appbar_share_err_remove".tr(), @@ -169,7 +168,7 @@ class AlbumViewerAppbar extends HookConsumerWidget ); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } void handleShareAssets( @@ -190,7 +189,7 @@ class AlbumViewerAppbar extends HookConsumerWidget gravity: ToastGravity.BOTTOM, ); } - Navigator.of(buildContext).pop(); + buildContext.pop(); }, ); return const ShareDialog(); @@ -200,9 +199,9 @@ class AlbumViewerAppbar extends HookConsumerWidget } void onShareAssetsTo() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; handleShareAssets(ref, context, selected); - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } buildBottomSheetActions() { @@ -212,36 +211,40 @@ class AlbumViewerAppbar extends HookConsumerWidget leading: const Icon(Icons.ios_share_rounded), title: const Text( 'album_viewer_appbar_share_to', - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle(fontWeight: FontWeight.w500), ).tr(), onTap: () => onShareAssetsTo(), ), - album.ownerId == userId ? ListTile( - leading: const Icon(Icons.delete_sweep_rounded), - title: const Text( - 'album_viewer_appbar_share_remove', - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(), - onTap: () => onRemoveFromAlbumPressed(), - ) : const SizedBox(), + album.ownerId == userId + ? ListTile( + leading: const Icon(Icons.delete_sweep_rounded), + title: const Text( + 'album_viewer_appbar_share_remove', + style: TextStyle(fontWeight: FontWeight.w500), + ).tr(), + onTap: () => onRemoveFromAlbumPressed(), + ) + : const SizedBox(), ]; } else { return [ - album.ownerId == userId ? ListTile( - leading: const Icon(Icons.delete_forever_rounded), - title: const Text( - 'album_viewer_appbar_share_delete', - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(), - onTap: () => onDeleteAlbumPressed(), - ) : ListTile( - leading: const Icon(Icons.person_remove_rounded), - title: const Text( - 'album_viewer_appbar_share_leave', - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(), - onTap: () => onLeaveAlbumPressed(), - ), + album.ownerId == userId + ? ListTile( + leading: const Icon(Icons.delete_forever_rounded), + title: const Text( + 'album_viewer_appbar_share_delete', + style: TextStyle(fontWeight: FontWeight.w500), + ).tr(), + onTap: () => onDeleteAlbumPressed(), + ) + : ListTile( + leading: const Icon(Icons.person_remove_rounded), + title: const Text( + 'album_viewer_appbar_share_leave', + style: TextStyle(fontWeight: FontWeight.w500), + ).tr(), + onTap: () => onLeaveAlbumPressed(), + ), ]; } } @@ -251,33 +254,31 @@ class AlbumViewerAppbar extends HookConsumerWidget ListTile( leading: const Icon(Icons.person_add_alt_rounded), onTap: () { - Navigator.pop(context); + context.pop(); onAddUsers!(album); }, title: const Text( "album_viewer_page_share_add_users", - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle(fontWeight: FontWeight.w500), ).tr(), ), ListTile( leading: const Icon(Icons.share_rounded), onTap: () { - AutoRouter.of(context) - .push(SharedLinkEditRoute(albumId: album.remoteId)); - Navigator.pop(context); + context.autoPush(SharedLinkEditRoute(albumId: album.remoteId)); + context.pop(); }, title: const Text( "control_bottom_app_bar_share", - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle(fontWeight: FontWeight.w500), ).tr(), ), ListTile( leading: const Icon(Icons.settings_rounded), - onTap: () => - AutoRouter.of(context).navigate(AlbumOptionsRoute(album: album)), + onTap: () => context.autoNavigate(AlbumOptionsRoute(album: album)), title: const Text( "translated_text_options", - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle(fontWeight: FontWeight.w500), ).tr(), ), ]; @@ -286,17 +287,17 @@ class AlbumViewerAppbar extends HookConsumerWidget ListTile( leading: const Icon(Icons.add_photo_alternate_outlined), onTap: () { - Navigator.pop(context); + context.pop(); onAddPhotos!(album); }, title: const Text( "share_add_photos", - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle(fontWeight: FontWeight.w500), ).tr(), ), ]; showModalBottomSheet( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, + backgroundColor: context.scaffoldBackgroundColor, isScrollControlled: false, context: context, builder: (context) { @@ -338,7 +339,7 @@ class AlbumViewerAppbar extends HookConsumerWidget comments.toString(), style: TextStyle( fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), ), @@ -377,7 +378,7 @@ class AlbumViewerAppbar extends HookConsumerWidget ); } else { return IconButton( - onPressed: () async => await AutoRouter.of(context).pop(), + onPressed: () async => await context.autoPop(), icon: const Icon(Icons.arrow_back_ios_rounded), splashRadius: 25, ); @@ -390,7 +391,8 @@ class AlbumViewerAppbar extends HookConsumerWidget title: selected.isNotEmpty ? Text('${selected.length}') : null, centerTitle: false, actions: [ - if (album.shared) buildActivitiesButton(), + if (album.shared && (album.activityEnabled || comments != 0)) + buildActivitiesButton(), if (album.isRemote) IconButton( splashRadius: 25, diff --git a/mobile/lib/modules/album/ui/album_viewer_editable_title.dart b/mobile/lib/modules/album/ui/album_viewer_editable_title.dart index 8a7e46f8c..1ece28afb 100644 --- a/mobile/lib/modules/album/ui/album_viewer_editable_title.dart +++ b/mobile/lib/modules/album/ui/album_viewer_editable_title.dart @@ -2,6 +2,7 @@ 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/modules/album/providers/album_viewer.provider.dart'; import 'package:immich_mobile/shared/models/album.dart'; @@ -17,7 +18,6 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final titleTextEditController = useTextEditingController(text: album.name); - final isDarkTheme = Theme.of(context).brightness == Brightness.dark; void onFocusModeChange() { if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) { @@ -44,7 +44,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { } }, focusNode: titleFocusNode, - style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + style: context.textTheme.headlineMedium, controller: titleTextEditController, onTap: () { FocusScope.of(context).requestFocus(titleFocusNode); @@ -65,7 +65,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { }, icon: Icon( Icons.cancel_rounded, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), splashRadius: 10, ) @@ -79,14 +79,14 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { borderRadius: BorderRadius.circular(10), ), focusColor: Colors.grey[300], - fillColor: isDarkTheme + fillColor: context.isDarkTheme ? const Color.fromARGB(255, 32, 33, 35) : Colors.grey[200], filled: titleFocusNode.hasFocus, hintText: 'share_add_title'.tr(), hintStyle: TextStyle( fontSize: 28, - color: isDarkTheme ? Colors.grey[300] : Colors.grey[700], + color: context.isDarkTheme ? Colors.grey[300] : Colors.grey[700], fontWeight: FontWeight.bold, ), ), diff --git a/mobile/lib/modules/album/views/album_options_part.dart b/mobile/lib/modules/album/views/album_options_part.dart index eb08b6bda..6ef773339 100644 --- a/mobile/lib/modules/album/views/album_options_part.dart +++ b/mobile/lib/modules/album/views/album_options_part.dart @@ -1,9 +1,9 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -23,28 +23,31 @@ class AlbumOptionsPage extends HookConsumerWidget { final sharedUsers = useState(album.sharedUsers.toList()); final owner = album.owner.value; final userId = ref.watch(authenticationProvider).userId; + final activityEnabled = useState(album.activityEnabled); + final isProcessing = useProcessingOverlay(); final isOwner = owner?.id == userId; void showErrorMessage() { - Navigator.pop(context); + context.pop(); ImmichToast.show( context: context, - msg: "Error leaving/removing from album", + msg: "shared_album_section_people_action_error".tr(), toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); } void leaveAlbum() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; try { final isSuccess = await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album); if (isSuccess) { - AutoRouter.of(context) - .navigate(const TabControllerRoute(children: [SharingRoute()])); + context.autoNavigate( + const TabControllerRoute(children: [SharingRoute()]), + ); } else { showErrorMessage(); } @@ -52,11 +55,11 @@ class AlbumOptionsPage extends HookConsumerWidget { showErrorMessage(); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } void removeUserFromAlbum(User user) async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; try { await ref @@ -68,8 +71,8 @@ class AlbumOptionsPage extends HookConsumerWidget { showErrorMessage(); } - Navigator.pop(context); - ImmichLoadingOverlayController.appLoader.hide(); + context.pop(); + isProcessing.value = false; } void handleUserClick(User user) { @@ -79,7 +82,7 @@ class AlbumOptionsPage extends HookConsumerWidget { actions = [ ListTile( leading: const Icon(Icons.exit_to_app_rounded), - title: const Text("Leave album"), + title: const Text("shared_album_section_people_action_leave").tr(), onTap: leaveAlbum, ), ]; @@ -89,14 +92,15 @@ class AlbumOptionsPage extends HookConsumerWidget { actions = [ ListTile( leading: const Icon(Icons.person_remove_rounded), - title: const Text("Remove user from album"), + title: const Text("shared_album_section_people_action_remove_user") + .tr(), onTap: () => removeUserFromAlbum(user), ), ]; } showModalBottomSheet( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, + backgroundColor: context.scaffoldBackgroundColor, isScrollControlled: false, context: context, builder: (context) { @@ -115,28 +119,22 @@ class AlbumOptionsPage extends HookConsumerWidget { buildOwnerInfo() { return ListTile( - leading: owner != null - ? UserCircleAvatar( - user: owner, - useRandomBackgroundColor: true, - ) - : const SizedBox(), + leading: + owner != null ? UserCircleAvatar(user: owner) : const SizedBox(), title: Text( - album.owner.value?.firstName ?? "", + album.owner.value?.name ?? "", style: const TextStyle( - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w500, ), ), subtitle: Text( album.owner.value?.email ?? "", - style: TextStyle(color: Colors.grey[500]), - ), - trailing: const Text( - "Owner", - style: TextStyle( - fontWeight: FontWeight.bold, - ), + style: TextStyle(color: Colors.grey[600]), ), + trailing: Text( + "shared_album_section_people_owner_label", + style: context.textTheme.labelLarge, + ).tr(), ); } @@ -149,18 +147,17 @@ class AlbumOptionsPage extends HookConsumerWidget { return ListTile( leading: UserCircleAvatar( user: user, - useRandomBackgroundColor: true, radius: 22, ), title: Text( - user.firstName, + user.name, style: const TextStyle( - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w500, ), ), subtitle: Text( user.email, - style: TextStyle(color: Colors.grey[500]), + style: TextStyle(color: Colors.grey[600]), ), trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) @@ -176,7 +173,7 @@ class AlbumOptionsPage extends HookConsumerWidget { buildSectionTitle(String text) { return Padding( padding: const EdgeInsets.all(16.0), - child: Text(text, style: Theme.of(context).textTheme.bodySmall), + child: Text(text, style: context.textTheme.bodySmall), ); } @@ -184,9 +181,7 @@ class AlbumOptionsPage extends HookConsumerWidget { appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back_ios_new_rounded), - onPressed: () { - AutoRouter.of(context).pop(null); - }, + onPressed: () => context.autoPop(null), ), centerTitle: true, title: Text("translated_text_options".tr()), @@ -195,7 +190,34 @@ class AlbumOptionsPage extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - buildSectionTitle("PEOPLE"), + if (isOwner && album.shared) + SwitchListTile.adaptive( + value: activityEnabled.value, + onChanged: (bool value) async { + activityEnabled.value = value; + if (await ref + .read(sharedAlbumProvider.notifier) + .setActivityEnabled(album, value)) { + album.activityEnabled = value; + } + }, + activeColor: activityEnabled.value + ? context.primaryColor + : context.themeData.disabledColor, + dense: true, + title: Text( + "shared_album_activity_setting_title", + style: context.textTheme.titleMedium + ?.copyWith(fontWeight: FontWeight.w500), + ).tr(), + subtitle: Text( + "shared_album_activity_setting_subtitle", + style: context.textTheme.labelLarge?.copyWith( + color: context.textTheme.labelLarge?.color?.withAlpha(175), + ), + ).tr(), + ), + buildSectionTitle("shared_album_section_people_title".tr()), buildOwnerInfo(), buildSharedUsersList(), ], diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart index dc30b3718..6d07c3b66 100644 --- a/mobile/lib/modules/album/views/album_viewer_page.dart +++ b/mobile/lib/modules/album/views/album_viewer_page.dart @@ -1,10 +1,11 @@ import 'dart:async'; -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; import 'package:immich_mobile/modules/album/services/album.service.dart'; @@ -17,7 +18,6 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; @@ -33,6 +33,7 @@ class AlbumViewerPage extends HookConsumerWidget { final userId = ref.watch(authenticationProvider).userId; final selection = useState>({}); final multiSelectEnabled = useState(false); + final isProcessing = useProcessingOverlay(); useEffect( () { @@ -67,7 +68,7 @@ class AlbumViewerPage extends HookConsumerWidget { /// If they exist, add to selected asset state to show they are already selected. void onAddPhotosPressed(Album albumInfo) async { AssetSelectionPageResult? returnPayload = - await AutoRouter.of(context).push( + await context.autoPush( AssetSelectionRoute( existingAssets: albumInfo.assets, canDeselect: false, @@ -75,35 +76,31 @@ class AlbumViewerPage extends HookConsumerWidget { ), ); - if (returnPayload != null) { + if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) { // Check if there is new assets add - if (returnPayload.selectedAssets.isNotEmpty) { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; - var addAssetsResult = - await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum( - returnPayload.selectedAssets, - albumInfo, - ); + var addAssetsResult = + await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum( + returnPayload.selectedAssets, + albumInfo, + ); - if (addAssetsResult != null && - addAssetsResult.successfullyAdded > 0) { - ref.invalidate(albumDetailProvider(albumId)); - } - - ImmichLoadingOverlayController.appLoader.hide(); + if (addAssetsResult != null && addAssetsResult.successfullyAdded > 0) { + ref.invalidate(albumDetailProvider(albumId)); } + + isProcessing.value = false; } } void onAddUsersPressed(Album album) async { - List? sharedUserIds = - await AutoRouter.of(context).push?>( + List? sharedUserIds = await context.autoPush?>( SelectAdditionalUserForSharingRoute(album: album), ); if (sharedUserIds != null) { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; var isSuccess = await ref .watch(albumServiceProvider) @@ -113,7 +110,7 @@ class AlbumViewerPage extends HookConsumerWidget { ref.invalidate(albumDetailProvider(album.id)); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } } @@ -154,10 +151,7 @@ class AlbumViewerPage extends HookConsumerWidget { padding: const EdgeInsets.only(left: 8.0), child: Text( album.name, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), + style: context.textTheme.headlineMedium, ), ), ); @@ -171,11 +165,19 @@ class AlbumViewerPage extends HookConsumerWidget { return const SizedBox(); } - final String startDateText = (startDate.year == endDate.year - ? DateFormat.MMMd() - : DateFormat.yMMMd()) - .format(startDate); - final String endDateText = DateFormat.yMMMd().format(endDate); + final String dateRangeText; + if (startDate.day == endDate.day && + startDate.month == endDate.month && + startDate.year == endDate.year) { + dateRangeText = DateFormat.yMMMd().format(startDate); + } else { + final String startDateText = (startDate.year == endDate.year + ? DateFormat.MMMd() + : DateFormat.yMMMd()) + .format(startDate); + final String endDateText = DateFormat.yMMMd().format(endDate); + dateRangeText = "$startDateText - $endDateText"; + } return Padding( padding: EdgeInsets.only( @@ -183,11 +185,8 @@ class AlbumViewerPage extends HookConsumerWidget { bottom: album.shared ? 0.0 : 8.0, ), child: Text( - "$startDateText - $endDateText", - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - ), + dateRangeText, + style: context.textTheme.labelLarge, ), ); } @@ -195,7 +194,7 @@ class AlbumViewerPage extends HookConsumerWidget { Widget buildSharedUserIconsRow(Album album) { return GestureDetector( onTap: () async { - await AutoRouter.of(context).push(AlbumOptionsRoute(album: album)); + await context.autoPush(AlbumOptionsRoute(album: album)); ref.invalidate(albumDetailProvider(album.id)); }, child: SizedBox( @@ -210,7 +209,6 @@ class AlbumViewerPage extends HookConsumerWidget { user: album.sharedUsers.toList()[index], radius: 18, size: 36, - useRandomBackgroundColor: true, ), ); }), @@ -234,11 +232,12 @@ class AlbumViewerPage extends HookConsumerWidget { onActivitiesPressed(Album album) { if (album.remoteId != null) { - AutoRouter.of(context).push( + context.autoPush( ActivitiesRoute( albumId: album.remoteId!, appBarTitle: album.name, isOwner: userId == album.ownerId, + isReadOnly: !album.activityEnabled, ), ); } @@ -259,13 +258,11 @@ class AlbumViewerPage extends HookConsumerWidget { error: (error, stackTrace) => AppBar(title: const Text("Error")), loading: () => AppBar(), ), - body: album.when( - data: (data) => WillPopScope( + body: album.widgetWhen( + onData: (data) => WillPopScope( onWillPop: onWillPop, child: GestureDetector( - onTap: () { - titleFocusNode.unfocus(); - }, + onTap: () => titleFocusNode.unfocus(), child: ImmichAssetGrid( renderList: data.renderList, listener: selectionListener, @@ -279,14 +276,11 @@ class AlbumViewerPage extends HookConsumerWidget { ], ), isOwner: userId == data.ownerId, - sharedAlbumId: data.remoteId, + sharedAlbumId: + data.shared && data.activityEnabled ? data.remoteId : null, ), ), ), - error: (e, _) => Center(child: Text("Error loading album info!\n$e")), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), ), ); } diff --git a/mobile/lib/modules/album/views/asset_selection_page.dart b/mobile/lib/modules/album/views/asset_selection_page.dart index 9c30ba80f..471a74ace 100644 --- a/mobile/lib/modules/album/views/asset_selection_page.dart +++ b/mobile/lib/modules/album/views/asset_selection_page.dart @@ -3,6 +3,8 @@ 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/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; @@ -78,18 +80,14 @@ class AssetSelectionPage extends HookConsumerWidget { canDeselect ? "share_done" : "share_add", style: TextStyle( fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ).tr(), ), ], ), - body: renderList.when( - data: (data) => buildBody(data), - error: (error, stackTrace) => Center( - child: Text(error.toString()), - ), - loading: () => const Center(child: CircularProgressIndicator()), + body: renderList.widgetWhen( + onData: (data) => buildBody(data), ), ); } diff --git a/mobile/lib/modules/album/views/create_album_page.dart b/mobile/lib/modules/album/views/create_album_page.dart index 191ce1470..7e5fb8168 100644 --- a/mobile/lib/modules/album/views/create_album_page.dart +++ b/mobile/lib/modules/album/views/create_album_page.dart @@ -1,8 +1,8 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_title.provider.dart'; @@ -34,11 +34,11 @@ class CreateAlbumPage extends HookConsumerWidget { final selectedAssets = useState>( initialAssets != null ? Set.from(initialAssets!) : const {}, ); - final isDarkTheme = Theme.of(context).brightness == Brightness.dark; showSelectUserPage() async { - final bool? ok = await AutoRouter.of(context) - .push(SelectUserForSharingRoute(assets: selectedAssets.value)); + final bool? ok = await context.autoPush( + SelectUserForSharingRoute(assets: selectedAssets.value), + ); if (ok == true) { selectedAssets.value = {}; } @@ -58,7 +58,7 @@ class CreateAlbumPage extends HookConsumerWidget { onSelectPhotosButtonPressed() async { AssetSelectionPageResult? selectedAsset = - await AutoRouter.of(context).push( + await context.autoPush( AssetSelectionRoute( existingAssets: selectedAssets.value, canDeselect: true, @@ -94,10 +94,7 @@ class CreateAlbumPage extends HookConsumerWidget { padding: const EdgeInsets.only(top: 200, left: 18), child: Text( 'create_shared_album_page_share_add_assets', - style: Theme.of(context).textTheme.displayMedium?.copyWith( - fontSize: 12, - fontWeight: FontWeight.normal, - ), + style: context.textTheme.labelLarge, ).tr(), ), ); @@ -117,9 +114,9 @@ class CreateAlbumPage extends HookConsumerWidget { padding: const EdgeInsets.symmetric(vertical: 22, horizontal: 16), side: BorderSide( - color: isDarkTheme + color: context.isDarkTheme ? const Color.fromARGB(255, 63, 63, 63) - : const Color.fromARGB(255, 206, 206, 206), + : const Color.fromARGB(255, 129, 129, 129), ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), @@ -128,16 +125,15 @@ class CreateAlbumPage extends HookConsumerWidget { onPressed: onSelectPhotosButtonPressed, icon: Icon( Icons.add_rounded, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), label: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( 'create_shared_album_page_share_select_photos', - style: Theme.of(context).textTheme.labelLarge?.copyWith( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleMedium?.copyWith( + color: context.primaryColor, + ), ).tr(), ), ), @@ -206,7 +202,7 @@ class CreateAlbumPage extends HookConsumerWidget { selectedAssets.value = {}; ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); - AutoRouter.of(context).replace(AlbumViewerRoute(albumId: newAlbum.id)); + context.autoReplace(AlbumViewerRoute(albumId: newAlbum.id)); } } @@ -214,19 +210,16 @@ class CreateAlbumPage extends HookConsumerWidget { appBar: AppBar( elevation: 0, centerTitle: false, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, + backgroundColor: context.scaffoldBackgroundColor, leading: IconButton( onPressed: () { selectedAssets.value = {}; - AutoRouter.of(context).pop(); + context.autoPop(); }, icon: const Icon(Icons.close_rounded), ), - title: Text( + title: const Text( 'share_create_album', - style: Theme.of(context).textTheme.displayMedium?.copyWith( - color: Theme.of(context).primaryColor, - ), ).tr(), actions: [ if (isSharedAlbum) @@ -239,8 +232,8 @@ class CreateAlbumPage extends HookConsumerWidget { style: TextStyle( fontWeight: FontWeight.bold, color: albumTitleController.text.isEmpty - ? Theme.of(context).disabledColor - : Theme.of(context).primaryColor, + ? context.themeData.disabledColor + : context.primaryColor, ), ), ), @@ -254,7 +247,7 @@ class CreateAlbumPage extends HookConsumerWidget { 'create_shared_album_page_create'.tr(), style: TextStyle( fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), ), @@ -265,7 +258,7 @@ class CreateAlbumPage extends HookConsumerWidget { child: CustomScrollView( slivers: [ SliverAppBar( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, + backgroundColor: context.scaffoldBackgroundColor, elevation: 5, automaticallyImplyLeading: false, pinned: true, diff --git a/mobile/lib/modules/album/views/library_page.dart b/mobile/lib/modules/album/views/library_page.dart index 90d095640..ce4d5e0cc 100644 --- a/mobile/lib/modules/album/views/library_page.dart +++ b/mobile/lib/modules/album/views/library_page.dart @@ -1,9 +1,9 @@ -import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -21,7 +21,7 @@ class LibraryPage extends HookConsumerWidget { final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); final albums = ref.watch(albumProvider); - var isDarkMode = Theme.of(context).brightness == Brightness.dark; + var isDarkTheme = context.isDarkTheme; var settings = ref.watch(appSettingsServiceProvider); useEffect( @@ -96,15 +96,14 @@ class LibraryPage extends HookConsumerWidget { padding: const EdgeInsets.only(right: 12.0), child: Icon( Icons.check, - color: selected - ? Theme.of(context).primaryColor - : Colors.transparent, + color: + selected ? context.primaryColor : Colors.transparent, ), ), Text( option, style: TextStyle( - color: selected ? Theme.of(context).primaryColor : null, + color: selected ? context.primaryColor : null, fontSize: 12.0, ), ), @@ -122,14 +121,12 @@ class LibraryPage extends HookConsumerWidget { Icon( Icons.swap_vert_rounded, size: 18, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), Text( options[selectedAlbumSortOrder.value], - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, - fontSize: 12.0, + style: context.textTheme.labelLarge?.copyWith( + color: context.primaryColor, ), ), ], @@ -140,7 +137,7 @@ class LibraryPage extends HookConsumerWidget { Widget buildCreateAlbumButton() { return GestureDetector( onTap: () { - AutoRouter.of(context).push(CreateAlbumRoute(isSharedAlbum: false)); + context.autoPush(CreateAlbumRoute(isSharedAlbum: false)); }, child: Padding( padding: const EdgeInsets.only(bottom: 32), @@ -152,18 +149,18 @@ class LibraryPage extends HookConsumerWidget { child: Container( decoration: BoxDecoration( border: Border.all( - color: isDarkMode + color: isDarkTheme ? const Color.fromARGB(255, 53, 53, 53) : const Color.fromARGB(255, 203, 203, 203), ), - color: isDarkMode ? Colors.grey[900] : Colors.grey[50], + color: isDarkTheme ? Colors.grey[900] : Colors.grey[50], borderRadius: BorderRadius.circular(20), ), child: Center( child: Icon( Icons.add_rounded, size: 28, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), ), @@ -173,11 +170,9 @@ class LibraryPage extends HookConsumerWidget { top: 8.0, bottom: 16, ), - child: const Text( + child: Text( 'library_page_new_album', - style: TextStyle( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.labelLarge, ).tr(), ), ], @@ -199,23 +194,23 @@ class LibraryPage extends HookConsumerWidget { child: Text( label, style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 13.0, - color: isDarkMode ? Colors.white : Colors.grey[800], + color: context.isDarkTheme + ? Colors.white + : Colors.black.withAlpha(200), ), ), ), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.grey[50], + backgroundColor: isDarkTheme ? Colors.grey[900] : Colors.grey[50], side: BorderSide( - color: isDarkMode ? Colors.grey[800]! : Colors.grey[300]!, + color: isDarkTheme ? Colors.grey[800]! : Colors.grey[300]!, ), alignment: Alignment.centerLeft, ), icon: Icon( icon, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), ); @@ -228,7 +223,7 @@ class LibraryPage extends HookConsumerWidget { Widget? shareTrashButton() { return trashEnabled ? InkWell( - onTap: () => AutoRouter.of(context).push(const TrashRoute()), + onTap: () => context.autoPush(const TrashRoute()), borderRadius: BorderRadius.circular(12), child: const Icon( Icons.delete_rounded, @@ -257,12 +252,12 @@ class LibraryPage extends HookConsumerWidget { children: [ buildLibraryNavButton( "library_page_favorites".tr(), Icons.favorite_border, () { - AutoRouter.of(context).navigate(const FavoritesRoute()); + context.autoNavigate(const FavoritesRoute()); }), const SizedBox(width: 12.0), buildLibraryNavButton( "library_page_archive".tr(), Icons.archive_outlined, () { - AutoRouter.of(context).navigate(const ArchiveRoute()); + context.autoNavigate(const ArchiveRoute()); }), ], ), @@ -279,9 +274,11 @@ class LibraryPage extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( 'library_page_albums', - style: TextStyle(fontWeight: FontWeight.bold), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), ).tr(), buildSortButton(), ], @@ -306,7 +303,7 @@ class LibraryPage extends HookConsumerWidget { return AlbumThumbnailCard( album: sorted[index - 1], - onTap: () => AutoRouter.of(context).push( + onTap: () => context.autoPush( AlbumViewerRoute( albumId: sorted[index - 1].id, ), @@ -327,9 +324,11 @@ class LibraryPage extends HookConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( 'library_page_device_albums', - style: TextStyle(fontWeight: FontWeight.bold), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), ).tr(), ], ), @@ -348,7 +347,7 @@ class LibraryPage extends HookConsumerWidget { childCount: local.length, (context, index) => AlbumThumbnailCard( album: local[index], - onTap: () => AutoRouter.of(context).push( + onTap: () => context.autoPush( AlbumViewerRoute( albumId: local[index].id, ), diff --git a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart index e13637d23..2aad67ef5 100644 --- a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart +++ b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart @@ -1,12 +1,12 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart'; import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/user.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; class SelectAdditionalUserForSharingPage extends HookConsumerWidget { @@ -22,14 +22,13 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { final sharedUsersList = useState>({}); addNewUsersHandler() { - AutoRouter.of(context) - .pop(sharedUsersList.value.map((e) => e.id).toList()); + context.autoPop(sharedUsersList.value.map((e) => e.id).toList()); } buildTileIcon(User user) { if (sharedUsersList.value.contains(user)) { return CircleAvatar( - backgroundColor: Theme.of(context).primaryColor, + backgroundColor: context.primaryColor, child: const Icon( Icons.check_rounded, size: 25, @@ -50,7 +49,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Chip( - backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15), + backgroundColor: context.primaryColor.withOpacity(0.15), label: Text( user.email, style: const TextStyle( @@ -124,7 +123,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { leading: IconButton( icon: const Icon(Icons.close_rounded), onPressed: () { - AutoRouter.of(context).pop(null); + context.autoPop(null); }, ), actions: [ @@ -138,8 +137,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { ), ], ), - body: suggestedShareUsers.when( - data: (users) { + body: suggestedShareUsers.widgetWhen( + onData: (users) { for (var sharedUsers in album.sharedUsers) { users.removeWhere( (u) => u.id == sharedUsers.id || u.id == album.ownerId, @@ -148,10 +147,6 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { return buildUserList(users); }, - error: (e, _) => Text("Error loading suggested users $e"), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), ), ); } diff --git a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart index fe6cef401..3d6dcf678 100644 --- a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart +++ b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart @@ -1,15 +1,15 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/providers/album_title.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/user.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; class SelectUserForSharingPage extends HookConsumerWidget { @@ -35,14 +35,19 @@ class SelectUserForSharingPage extends HookConsumerWidget { await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums(); // ref.watch(assetSelectionProvider.notifier).removeAll(); ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); - AutoRouter.of(context).pop(true); - AutoRouter.of(context) - .navigate(const TabControllerRoute(children: [SharingRoute()])); + context.autoPop(true); + context + .autoNavigate(const TabControllerRoute(children: [SharingRoute()])); } ScaffoldMessenger( child: SnackBar( - content: const Text('select_user_for_sharing_page_err_album').tr(), + content: Text( + 'select_user_for_sharing_page_err_album', + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ).tr(), ), ); } @@ -50,7 +55,7 @@ class SelectUserForSharingPage extends HookConsumerWidget { buildTileIcon(User user) { if (sharedUsersList.value.contains(user)) { return CircleAvatar( - backgroundColor: Theme.of(context).primaryColor, + backgroundColor: context.primaryColor, child: const Icon( Icons.check_rounded, size: 25, @@ -71,7 +76,7 @@ class SelectUserForSharingPage extends HookConsumerWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Chip( - backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15), + backgroundColor: context.primaryColor.withOpacity(0.15), label: Text( user.email, style: const TextStyle( @@ -139,20 +144,20 @@ class SelectUserForSharingPage extends HookConsumerWidget { appBar: AppBar( title: Text( 'share_invite', - style: TextStyle(color: Theme.of(context).primaryColor), + style: TextStyle(color: context.primaryColor), ).tr(), elevation: 0, centerTitle: false, leading: IconButton( icon: const Icon(Icons.close_rounded), onPressed: () async { - AutoRouter.of(context).pop(); + context.autoPop(); }, ), actions: [ TextButton( style: TextButton.styleFrom( - foregroundColor: Theme.of(context).primaryColor, + foregroundColor: context.primaryColor, ), onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum, child: const Text( @@ -160,20 +165,16 @@ class SelectUserForSharingPage extends HookConsumerWidget { style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, - // color: Theme.of(context).primaryColor, + // color: context.primaryColor, ), ).tr(), ), ], ), - body: suggestedShareUsers.when( - data: (users) { + body: suggestedShareUsers.widgetWhen( + onData: (users) { return buildUserList(users); }, - error: (e, _) => Text("Error loading suggested users $e"), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), ), ); } diff --git a/mobile/lib/modules/album/views/sharing_page.dart b/mobile/lib/modules/album/views/sharing_page.dart index 5e74aef67..2e2e44aca 100644 --- a/mobile/lib/modules/album/views/sharing_page.dart +++ b/mobile/lib/modules/album/views/sharing_page.dart @@ -1,8 +1,8 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart'; import 'package:immich_mobile/modules/partner/providers/partner.provider.dart'; @@ -21,7 +21,6 @@ class SharingPage extends HookConsumerWidget { final List sharedAlbums = ref.watch(sharedAlbumProvider); final userId = ref.watch(currentUserProvider)?.id; final partner = ref.watch(partnerSharedWithProvider); - var isDarkMode = Theme.of(context).brightness == Brightness.dark; useEffect( () { @@ -47,8 +46,9 @@ class SharingPage extends HookConsumerWidget { album: sharedAlbums[index], showOwner: true, onTap: () { - AutoRouter.of(context) - .push(AlbumViewerRoute(albumId: sharedAlbums[index].id)); + context.autoPush( + AlbumViewerRoute(albumId: sharedAlbums[index].id), + ); }, ); }, @@ -79,32 +79,27 @@ class SharingPage extends HookConsumerWidget { album.name, maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.bold, - color: isDarkMode - ? Theme.of(context).primaryColor - : Colors.black, - ), + style: context.textTheme.bodyMedium?.copyWith( + color: context.primaryColor, + fontWeight: FontWeight.w500, + ), ), subtitle: isOwner ? Text( 'album_thumbnail_owned'.tr(), - style: const TextStyle( - fontSize: 12.0, - ), + style: context.textTheme.bodyMedium, ) : album.ownerName != null ? Text( 'album_thumbnail_shared_by' .tr(args: [album.ownerName!]), - style: const TextStyle( - fontSize: 12.0, - ), + style: context.textTheme.bodyMedium, ) : null, onTap: () { - AutoRouter.of(context) - .push(AlbumViewerRoute(albumId: sharedAlbums[index].id)); + context.autoPush( + AlbumViewerRoute(albumId: sharedAlbums[index].id), + ); }, ); }, @@ -127,8 +122,7 @@ class SharingPage extends HookConsumerWidget { Expanded( child: ElevatedButton.icon( onPressed: () { - AutoRouter.of(context) - .push(CreateAlbumRoute(isSharedAlbum: true)); + context.autoPush(CreateAlbumRoute(isSharedAlbum: true)); }, icon: const Icon( Icons.photo_album_outlined, @@ -138,8 +132,8 @@ class SharingPage extends HookConsumerWidget { "sharing_silver_appbar_create_shared_album", maxLines: 1, style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 11, + fontWeight: FontWeight.w500, + fontSize: 12, ), ).tr(), ), @@ -147,8 +141,7 @@ class SharingPage extends HookConsumerWidget { const SizedBox(width: 12.0), Expanded( child: ElevatedButton.icon( - onPressed: () => - AutoRouter.of(context).push(const SharedLinkRoute()), + onPressed: () => context.autoPush(const SharedLinkRoute()), icon: const Icon( Icons.link, size: 20, @@ -156,8 +149,8 @@ class SharingPage extends HookConsumerWidget { label: const Text( "sharing_silver_appbar_shared_links", style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 11, + fontWeight: FontWeight.w500, + fontSize: 12, ), maxLines: 1, ).tr(), @@ -191,21 +184,21 @@ class SharingPage extends HookConsumerWidget { child: Icon( Icons.insert_photo_rounded, size: 50, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), Padding( padding: const EdgeInsets.all(8.0), child: Text( 'sharing_page_empty_list', - style: Theme.of(context).textTheme.displaySmall, + style: context.textTheme.displaySmall, ).tr(), ), Padding( padding: const EdgeInsets.all(8.0), child: Text( 'sharing_page_description', - style: Theme.of(context).textTheme.bodyMedium, + style: context.textTheme.bodyMedium, ).tr(), ), ], @@ -218,7 +211,7 @@ class SharingPage extends HookConsumerWidget { Widget sharePartnerButton() { return InkWell( - onTap: () => AutoRouter.of(context).push(const PartnerRoute()), + onTap: () => context.autoPush(const PartnerRoute()), borderRadius: BorderRadius.circular(12), child: const Icon( Icons.swap_horizontal_circle_rounded, @@ -238,9 +231,11 @@ class SharingPage extends HookConsumerWidget { SliverPadding( padding: const EdgeInsets.all(12), sliver: SliverToBoxAdapter( - child: const Text( + child: Text( "partner_page_title", - style: TextStyle(fontWeight: FontWeight.bold), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), ).tr(), ), ), @@ -248,10 +243,10 @@ class SharingPage extends HookConsumerWidget { SliverPadding( padding: const EdgeInsets.all(12), sliver: SliverToBoxAdapter( - child: const Text( + child: Text( "sharing_page_album", - style: TextStyle( - fontWeight: FontWeight.bold, + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, ), ).tr(), ), diff --git a/mobile/lib/modules/archive/providers/archive_asset_provider.dart b/mobile/lib/modules/archive/providers/archive_asset_provider.dart index 328b3c7b5..579ace7c5 100644 --- a/mobile/lib/modules/archive/providers/archive_asset_provider.dart +++ b/mobile/lib/modules/archive/providers/archive_asset_provider.dart @@ -1,15 +1,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart'; +import 'package:immich_mobile/utils/renderlist_generator.dart'; import 'package:isar/isar.dart'; -final archiveProvider = StreamProvider((ref) async* { +final archiveProvider = StreamProvider((ref) { final user = ref.watch(currentUserProvider); - if (user == null) return; + if (user == null) return const Stream.empty(); final query = ref .watch(dbProvider) .assets @@ -19,11 +18,5 @@ final archiveProvider = StreamProvider((ref) async* { .isArchivedEqualTo(true) .isTrashedEqualTo(false) .sortByFileCreatedAt(); - final settings = ref.watch(appSettingsServiceProvider); - final groupBy = - GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - yield await RenderList.fromQuery(query, groupBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupBy); - } + return renderListGenerator(query, ref); }); diff --git a/mobile/lib/modules/archive/views/archive_page.dart b/mobile/lib/modules/archive/views/archive_page.dart index 2e1c1cd4a..fb3cecc10 100644 --- a/mobile/lib/modules/archive/views/archive_page.dart +++ b/mobile/lib/modules/archive/views/archive_page.dart @@ -1,8 +1,9 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/shared/models/asset.dart'; @@ -30,7 +31,7 @@ class ArchivePage extends HookConsumerWidget { AppBar buildAppBar(String count) { return AppBar( leading: IconButton( - onPressed: () => AutoRouter.of(context).pop(), + onPressed: () => context.autoPop(), icon: const Icon(Icons.arrow_back_ios_rounded), ), centerTitle: true, @@ -48,37 +49,33 @@ class ArchivePage extends HookConsumerWidget { child: SizedBox( height: 64, child: Card( - child: Column( - children: [ - ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - leading: const Icon( - Icons.unarchive_rounded, - ), - title: Text( - 'control_bottom_app_bar_unarchive'.tr(), - style: const TextStyle(fontSize: 14), - ), - onTap: processing.value - ? null - : () async { - processing.value = true; - try { - await handleArchiveAssets( - ref, - context, - selection.value.toList(), - shouldArchive: false, - ); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - }, - ), - ], + child: ListTile( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + leading: const Icon( + Icons.unarchive_rounded, + ), + title: Text( + 'control_bottom_app_bar_unarchive'.tr(), + style: const TextStyle(fontSize: 14), + ), + onTap: processing.value + ? null + : () async { + processing.value = true; + try { + await handleArchiveAssets( + ref, + context, + selection.value.toList(), + shouldArchive: false, + ); + } finally { + processing.value = false; + selectionEnabledHook.value = false; + } + }, ), ), ), @@ -86,18 +83,13 @@ class ArchivePage extends HookConsumerWidget { ); } - return archivedAssets.when( - loading: () => Scaffold( - appBar: buildAppBar("?"), - body: const Center(child: CircularProgressIndicator()), + return Scaffold( + appBar: archivedAssets.maybeWhen( + data: (data) => buildAppBar(data.totalAssets.toString()), + orElse: () => buildAppBar("?"), ), - error: (error, stackTrace) => Scaffold( - appBar: buildAppBar("Error"), - body: Center(child: Text(error.toString())), - ), - data: (data) => Scaffold( - appBar: buildAppBar(data.totalAssets.toString()), - body: data.isEmpty + body: archivedAssets.widgetWhen( + onData: (data) => data.isEmpty ? Center( child: Text('archive_page_no_archived_assets'.tr()), ) diff --git a/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart b/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart index 6df633f12..06cc128f7 100644 --- a/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart +++ b/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/services/album.service.dart'; import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart'; @@ -67,7 +68,7 @@ class ImageViewerStateNotifier extends StateNotifier { gravity: ToastGravity.BOTTOM, ); } - Navigator.of(buildContext).pop(); + buildContext.pop(); }, ); return const ShareDialog(); diff --git a/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart b/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart index 04532ce1b..c2e8782bb 100644 --- a/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart +++ b/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart @@ -3,6 +3,7 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structu import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/utils/renderlist_generator.dart'; import 'package:isar/isar.dart'; final renderListProvider = @@ -17,16 +18,6 @@ final renderListProvider = final renderListQueryProvider = StreamProvider.family?>( - (ref, query) async* { - if (query == null) { - return; - } - final settings = ref.watch(appSettingsServiceProvider); - final groupBy = GroupAssetsBy - .values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - yield await RenderList.fromQuery(query, groupBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupBy); - } - }, + (ref, query) => + query == null ? const Stream.empty() : renderListGenerator(query, ref), ); diff --git a/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart b/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart index 3a356c840..27be7029d 100644 --- a/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart +++ b/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; @@ -84,8 +83,8 @@ class ImageViewerService { } return entity != null; } - } catch (e) { - debugPrint("Error saving file $e"); + } catch (error, stack) { + _log.severe("Error saving file ${error.toString()}", error, stack); return false; } } diff --git a/mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart index 441492c39..c265346b0 100644 --- a/mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart +++ b/mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/models/asset.dart'; class AdvancedBottomSheet extends HookConsumerWidget { @@ -11,8 +12,6 @@ class AdvancedBottomSheet extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - var isDarkMode = Theme.of(context).brightness == Brightness.dark; - return SingleChildScrollView( child: Card( shape: const RoundedRectangleBorder( @@ -40,7 +39,9 @@ class AdvancedBottomSheet extends HookConsumerWidget { const SizedBox(height: 32.0), Container( decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.grey[200], + color: context.isDarkTheme + ? Colors.grey[900] + : Colors.grey[200], borderRadius: BorderRadius.circular(15.0), ), child: Padding( @@ -61,8 +62,14 @@ class AdvancedBottomSheet extends HookConsumerWidget { ClipboardData(text: assetDetail.toString()), ).then((_) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Copied to clipboard"), + SnackBar( + content: Text( + "Copied to clipboard", + style: context.textTheme.bodyLarge + ?.copyWith( + color: context.primaryColor, + ), + ), ), ); }); @@ -70,7 +77,7 @@ class AdvancedBottomSheet extends HookConsumerWidget { icon: Icon( Icons.copy, size: 16.0, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), ), diff --git a/mobile/lib/modules/asset_viewer/ui/description_input.dart b/mobile/lib/modules/asset_viewer/ui/description_input.dart index e00739faf..c5972a822 100644 --- a/mobile/lib/modules/asset_viewer/ui/description_input.dart +++ b/mobile/lib/modules/asset_viewer/ui/description_input.dart @@ -2,6 +2,7 @@ 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/modules/asset_viewer/providers/asset_description.provider.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart'; @@ -19,8 +20,7 @@ class DescriptionInput extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isDarkTheme = Theme.of(context).brightness == Brightness.dark; - final textColor = isDarkTheme ? Colors.white : Colors.black; + final textColor = context.isDarkTheme ? Colors.white : Colors.black; final controller = useTextEditingController(); final focusNode = useFocusNode(); final isFocus = useState(false); @@ -93,15 +93,11 @@ class DescriptionInput extends HookConsumerWidget { maxLines: null, keyboardType: TextInputType.multiline, controller: controller, - style: const TextStyle( - fontSize: 14, - ), + style: context.textTheme.labelLarge, decoration: InputDecoration( hintText: 'description_input_hint_text'.tr(), border: InputBorder.none, - hintStyle: TextStyle( - fontWeight: FontWeight.normal, - fontSize: 12, + hintStyle: context.textTheme.labelLarge?.copyWith( color: textColor.withOpacity(0.5), ), suffixIcon: suffixIcon, diff --git a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart index f194738a2..08a6a0515 100644 --- a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart +++ b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart @@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:timezone/timezone.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart'; import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart'; @@ -28,7 +29,7 @@ class ExifBottomSheet extends HookConsumerWidget { exifInfo.longitude != 0; String formatTimeZone(Duration d) => - "GMT${d.isNegative ? '-': '+'}${d.inHours.abs().toString().padLeft(2, '0')}:${d.inMinutes.abs().remainder(60).toString().padLeft(2, '0')}"; + "GMT${d.isNegative ? '-' : '+'}${d.inHours.abs().toString().padLeft(2, '0')}:${d.inMinutes.abs().remainder(60).toString().padLeft(2, '0')}"; String get formattedDateTime { DateTime dt = asset.fileCreatedAt.toLocal(); @@ -41,10 +42,16 @@ class ExifBottomSheet extends HookConsumerWidget { final location = getLocation(asset.exifInfo!.timeZone!); dt = TZDateTime.from(dt, location); } on LocationNotFoundException { - RegExp re = RegExp(r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$', caseSensitive: false); + RegExp re = RegExp( + r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$', + caseSensitive: false, + ); final m = re.firstMatch(asset.exifInfo!.timeZone!); if (m != null) { - final duration = Duration(hours: int.parse(m.group(1) ?? '0'), minutes: int.parse(m.group(2) ?? '0')); + final duration = Duration( + hours: int.parse(m.group(1) ?? '0'), + minutes: int.parse(m.group(2) ?? '0'), + ); dt = dt.add(duration); timeZone = formatTimeZone(duration); } @@ -105,8 +112,7 @@ class ExifBottomSheet extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final assetWithExif = ref.watch(assetDetailProvider(asset)); final exifInfo = (assetWithExif.value ?? asset).exifInfo; - var isDarkTheme = Theme.of(context).brightness == Brightness.dark; - var textColor = isDarkTheme ? Colors.white : Colors.black; + var textColor = context.isDarkTheme ? Colors.white : Colors.black; buildMap() { return Padding( @@ -187,21 +193,15 @@ class ExifBottomSheet extends HookConsumerWidget { children: [ Text( "exif_bottom_sheet_location", - style: TextStyle( - fontSize: 11, - color: textColor, - fontWeight: FontWeight.bold, + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, ), ).tr(), buildMap(), RichText( text: TextSpan( - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: textColor, - fontFamily: 'WorkSans', - ), + style: context.textTheme.labelLarge, children: [ if (exifInfo != null && exifInfo.city != null) TextSpan( @@ -222,7 +222,9 @@ class ExifBottomSheet extends HookConsumerWidget { ), Text( "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}", - style: const TextStyle(fontSize: 12), + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(150), + ), ), ], ), @@ -252,10 +254,7 @@ class ExifBottomSheet extends HookConsumerWidget { titleAlignment: ListTileTitleAlignment.center, title: Text( title, - style: TextStyle( - fontWeight: FontWeight.bold, - color: textColor, - ), + style: context.textTheme.labelLarge, ), subtitle: subtitle, ); @@ -272,7 +271,7 @@ class ExifBottomSheet extends HookConsumerWidget { // There is both filename and size information return createImagePropertiesListStyle( asset.fileName, - Text(imgSizeString), + Text(imgSizeString, style: context.textTheme.bodySmall), ); } else if (imgSizeString != null && asset.fileName.isEmpty) { // There is only size information @@ -299,10 +298,9 @@ class ExifBottomSheet extends HookConsumerWidget { padding: const EdgeInsets.only(bottom: 8.0), child: Text( "exif_bottom_sheet_details", - style: TextStyle( - fontSize: 11, - color: textColor, - fontWeight: FontWeight.bold, + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, ), ).tr(), ), @@ -317,14 +315,17 @@ class ExifBottomSheet extends HookConsumerWidget { ), title: Text( "${exifInfo!.make} ${exifInfo.model}", - style: TextStyle( - color: textColor, - fontWeight: FontWeight.bold, - ), + style: context.textTheme.labelLarge, ), - subtitle: exifInfo.f != null || exifInfo.exposureSeconds != null || exifInfo.mm != null || exifInfo.iso != null ? Text( - "ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", - ) : null, + subtitle: exifInfo.f != null || + exifInfo.exposureSeconds != null || + exifInfo.mm != null || + exifInfo.iso != null + ? Text( + "ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", + style: context.textTheme.bodySmall, + ) + : null, ), ], ); @@ -393,7 +394,7 @@ class ExifBottomSheet extends HookConsumerWidget { data: (data) => DescriptionInput(asset: data), error: (error, stackTrace) => Icon( Icons.image_not_supported_outlined, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), loading: () => const SizedBox( width: 75, diff --git a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart index 95965f6d8..3e6dfe2ee 100644 --- a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart +++ b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart @@ -1,6 +1,6 @@ -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/modules/activities/providers/activity.provider.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; @@ -147,7 +147,7 @@ class TopControlAppBar extends HookConsumerWidget { Widget buildBackButton() { return IconButton( onPressed: () { - AutoRouter.of(context).pop(); + context.autoPop(); }, icon: Icon( Icons.arrow_back_ios_new_rounded, diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index f8acef588..9948ab4cc 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart'; @@ -209,7 +210,7 @@ class GalleryViewerPage extends HookConsumerWidget { if (isDeleted && isParent) { if (totalAssets == 1) { // Handle only one asset - AutoRouter.of(context).pop(); + context.autoPop(); } else { // Go to next page otherwise controller.nextPage( @@ -293,7 +294,7 @@ class GalleryViewerPage extends HookConsumerWidget { final ratio = d.dy / max(d.dx.abs(), 1); if (d.dy > sensitivity && ratio > ratioThreshold) { - AutoRouter.of(context).pop(); + context.autoPop(); } else if (d.dy < -sensitivity && ratio < -ratioThreshold) { showInfo(); } @@ -308,7 +309,7 @@ class GalleryViewerPage extends HookConsumerWidget { .watch(assetProvider.notifier) .toggleArchive([asset], !asset.isArchived); if (isParent) { - AutoRouter.of(context).pop(); + context.autoPop(); return; } removeAssetFromStack(); @@ -331,7 +332,7 @@ class GalleryViewerPage extends HookConsumerWidget { handleActivities() { if (sharedAlbumId != null) { - AutoRouter.of(context).push( + context.autoPush( ActivitiesRoute( albumId: sharedAlbumId!, assetId: asset().remoteId, @@ -513,8 +514,8 @@ class GalleryViewerPage extends HookConsumerWidget { currentAsset, stackElements.elementAt(stackIndex.value), ); - Navigator.pop(ctx); - AutoRouter.of(context).pop(); + ctx.pop(); + context.autoPop(); }, title: const Text( "viewer_stack_use_as_main_asset", @@ -540,8 +541,8 @@ class GalleryViewerPage extends HookConsumerWidget { stackElements.elementAt(1), childrenToRemove: [currentAsset], ); - Navigator.pop(ctx); - AutoRouter.of(context).pop(); + ctx.pop(); + context.autoPop(); } else { await ref.read(assetStackServiceProvider).updateStack( currentAsset, @@ -550,7 +551,7 @@ class GalleryViewerPage extends HookConsumerWidget { ], ); removeAssetFromStack(); - Navigator.pop(ctx); + ctx.pop(); } }, title: const Text( @@ -568,8 +569,8 @@ class GalleryViewerPage extends HookConsumerWidget { currentAsset, childrenToRemove: stack, ); - Navigator.pop(ctx); - AutoRouter.of(context).pop(); + ctx.pop(); + context.autoPop(); }, title: const Text( "viewer_unstack", @@ -829,8 +830,8 @@ class GalleryViewerPage extends HookConsumerWidget { placeholder: Image( image: provider, fit: BoxFit.fitWidth, - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, + height: context.height, + width: context.width, alignment: Alignment.center, ), onVideoEnded: () { diff --git a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart index a56d65595..ad929a8c5 100644 --- a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:chewie/chewie.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart'; @@ -44,7 +45,7 @@ class VideoViewerPage extends HookConsumerWidget { ), error: (error, stackTrace) => Icon( Icons.image_not_supported_outlined, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), loading: () => const Center( child: SizedBox( @@ -74,8 +75,8 @@ class VideoViewerPage extends HookConsumerWidget { ), if (downloadAssetStatus == DownloadAssetStatus.loading) SizedBox( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, + height: context.height, + width: context.width, child: const Center( child: ImmichLoadingIndicator(), ), @@ -205,8 +206,8 @@ class _VideoPlayerState extends State { ); } else { return SizedBox( - height: MediaQuery.of(context).size.height, - width: MediaQuery.of(context).size.width, + height: context.height, + width: context.width, child: Center( child: Stack( children: [ diff --git a/mobile/lib/modules/backup/providers/backup.provider.dart b/mobile/lib/modules/backup/providers/backup.provider.dart index 0df8bc90d..3d8d6414c 100644 --- a/mobile/lib/modules/backup/providers/backup.provider.dart +++ b/mobile/lib/modules/backup/providers/backup.provider.dart @@ -89,7 +89,6 @@ class BackupNotifier extends StateNotifier { state = state .copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album}); - _updateBackupAssetCount(); } void addExcludedAlbumForBackup(AvailableAlbum album) { @@ -98,7 +97,6 @@ class BackupNotifier extends StateNotifier { } state = state .copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album}); - _updateBackupAssetCount(); } void removeAlbumForBackup(AvailableAlbum album) { @@ -107,7 +105,6 @@ class BackupNotifier extends StateNotifier { currentSelectedAlbums.removeWhere((a) => a == album); state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums); - _updateBackupAssetCount(); } void removeExcludedAlbumForBackup(AvailableAlbum album) { @@ -116,7 +113,20 @@ class BackupNotifier extends StateNotifier { currentExcludedAlbums.removeWhere((a) => a == album); state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums); - _updateBackupAssetCount(); + } + + Future backupAlbumSelectionDone() { + if (state.selectedBackupAlbums.isEmpty) { + // disable any backup + cancelBackup(); + setAutoBackup(false); + configureBackgroundBackup( + enabled: false, + onError: (msg) {}, + onBatteryInfo: () {}, + ); + } + return _updateBackupAssetCount(); } void setAutoBackup(bool enabled) { @@ -249,30 +259,6 @@ class BackupNotifier extends StateNotifier { final List selectedBackupAlbums = await _backupService.selectedAlbumsQuery().findAll(); - // First time backup - set isAll album is the default one for backup. - if (selectedBackupAlbums.isEmpty) { - log.info("First time backup; setup 'Recent(s)' album as default"); - - // Get album that contains all assets - final list = await PhotoManager.getAssetPathList( - hasAll: true, - onlyAll: true, - type: RequestType.common, - ); - - if (list.isEmpty) { - return; - } - AssetPathEntity albumHasAllAssets = list.first; - - final ba = BackupAlbum( - albumHasAllAssets.id, - DateTime.fromMillisecondsSinceEpoch(0), - BackupSelection.select, - ); - await _db.writeTxn(() => _db.backupAlbums.put(ba)); - } - // Generate AssetPathEntity from id to add to local state final Set selectedAlbums = {}; for (final BackupAlbum ba in selectedBackupAlbums) { @@ -362,7 +348,6 @@ class BackupNotifier extends StateNotifier { allUniqueAssets: {}, selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets, ); - return; } else { state = state.copyWith( allAssetsInDatabase: allAssetsInDatabase, @@ -373,8 +358,6 @@ class BackupNotifier extends StateNotifier { // Save to persistent storage await _updatePersistentAlbumsSelection(); - - return; } /// Get all necessary information for calculating the available albums, diff --git a/mobile/lib/modules/backup/providers/manual_upload.provider.dart b/mobile/lib/modules/backup/providers/manual_upload.provider.dart index 6d585e289..4d643b9a1 100644 --- a/mobile/lib/modules/backup/providers/manual_upload.provider.dart +++ b/mobile/lib/modules/backup/providers/manual_upload.provider.dart @@ -274,7 +274,7 @@ class ManualUploadNotifier extends StateNotifier { // The app is currently in background. Perform the necessary cleanups which // are on-hold for upload completion if (appState != AppStateEnum.active && appState != AppStateEnum.resumed) { - ref.read(appStateProvider.notifier).handleAppInactivity(); + ref.read(backupProvider.notifier).cancelBackup(); } } diff --git a/mobile/lib/modules/backup/services/backup.service.dart b/mobile/lib/modules/backup/services/backup.service.dart index 15cc0c349..f4ca5932a 100644 --- a/mobile/lib/modules/backup/services/backup.service.dart +++ b/mobile/lib/modules/backup/services/backup.service.dart @@ -42,6 +42,9 @@ class BackupService { try { return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId); + + // TODO! Start using this in 1.92.0 + // return await _apiService.assetApi.getAllUserAssetsByDeviceId(deviceId); } catch (e) { debugPrint('Error [getDeviceBackupAsset] ${e.toString()}'); return null; @@ -275,13 +278,6 @@ class BackupService { req.files.add(assetRawUploadData); - if (entity.isLivePhoto) { - var livePhotoRawUploadData = await _getLivePhotoFile(entity); - if (livePhotoRawUploadData != null) { - req.files.add(livePhotoRawUploadData); - } - } - setCurrentUploadAssetCb( CurrentUploadAsset( id: entity.id, @@ -296,6 +292,29 @@ class BackupService { var response = await httpClient.send(req, cancellationToken: cancelToken); + // Send live photo separately + if (entity.isLivePhoto) { + var livePhotoRawUploadData = await _getLivePhotoFile(entity); + if (livePhotoRawUploadData != null) { + var livePhotoReq = MultipartRequest( + req.method, + req.url, + onProgress: req.onProgress, + ) + ..headers.addAll(req.headers) + ..fields.addAll(req.fields); + + livePhotoReq.files.add(livePhotoRawUploadData); + // Send live photo only if the non-motion part is successful + if (response.statusCode == 200 || response.statusCode == 201) { + response = await httpClient.send( + livePhotoReq, + cancellationToken: cancelToken, + ); + } + } + } + if (response.statusCode == 200) { // asset is a duplicate (already exists on the server) duplicatedAssetIds.add(entity.id); @@ -353,7 +372,7 @@ class BackupService { var fileStream = motionFile.openRead(); String fileName = p.basename(motionFile.path); return http.MultipartFile( - "livePhotoData", + "assetData", fileStream, motionFile.lengthSync(), filename: fileName, diff --git a/mobile/lib/modules/backup/ui/album_info_card.dart b/mobile/lib/modules/backup/ui/album_info_card.dart index eaace503e..3e579a84c 100644 --- a/mobile/lib/modules/backup/ui/album_info_card.dart +++ b/mobile/lib/modules/backup/ui/album_info_card.dart @@ -1,9 +1,9 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -22,10 +22,10 @@ class AlbumInfoCard extends HookConsumerWidget { ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo); final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo); - final isDarkTheme = Theme.of(context).brightness == Brightness.dark; + final isDarkTheme = context.isDarkTheme; ColorFilter selectedFilter = ColorFilter.mode( - Theme.of(context).primaryColor.withAlpha(100), + context.primaryColor.withAlpha(100), BlendMode.darken, ); ColorFilter excludedFilter = @@ -46,7 +46,7 @@ class AlbumInfoCard extends HookConsumerWidget { fontWeight: FontWeight.bold, ), ).tr(), - backgroundColor: Theme.of(context).primaryColor, + backgroundColor: context.primaryColor, ); } else if (isExcluded) { return Chip( @@ -82,19 +82,9 @@ class AlbumInfoCard extends HookConsumerWidget { HapticFeedback.selectionClick(); if (isSelected) { - if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) { - ImmichToast.show( - context: context, - msg: "backup_err_only_album".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.watch(backupProvider.notifier).removeAlbumForBackup(albumInfo); + ref.read(backupProvider.notifier).removeAlbumForBackup(albumInfo); } else { - ref.watch(backupProvider.notifier).addAlbumForBackup(albumInfo); + ref.read(backupProvider.notifier).addAlbumForBackup(albumInfo); } }, onDoubleTap: () { @@ -103,23 +93,10 @@ class AlbumInfoCard extends HookConsumerWidget { if (isExcluded) { // Remove from exclude album list ref - .watch(backupProvider.notifier) + .read(backupProvider.notifier) .removeExcludedAlbumForBackup(albumInfo); } else { // Add to exclude album list - if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 && - ref - .watch(backupProvider) - .selectedBackupAlbums - .contains(albumInfo)) { - ImmichToast.show( - context: context, - msg: "backup_err_only_album".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') { ImmichToast.show( @@ -132,7 +109,7 @@ class AlbumInfoCard extends HookConsumerWidget { } ref - .watch(backupProvider.notifier) + .read(backupProvider.notifier) .addExcludedAlbumForBackup(albumInfo); } }, @@ -194,7 +171,7 @@ class AlbumInfoCard extends HookConsumerWidget { albumInfo.name, style: TextStyle( fontSize: 14, - color: Theme.of(context).primaryColor, + color: context.primaryColor, fontWeight: FontWeight.bold, ), ), @@ -224,13 +201,13 @@ class AlbumInfoCard extends HookConsumerWidget { ), IconButton( onPressed: () { - AutoRouter.of(context).push( + context.autoPush( AlbumPreviewRoute(album: albumInfo.albumEntity), ); }, icon: Icon( Icons.image_outlined, - color: Theme.of(context).primaryColor, + color: context.primaryColor, size: 24, ), splashRadius: 25, diff --git a/mobile/lib/modules/backup/ui/album_info_list_tile.dart b/mobile/lib/modules/backup/ui/album_info_list_tile.dart index 88141a015..0c27ca1ba 100644 --- a/mobile/lib/modules/backup/ui/album_info_list_tile.dart +++ b/mobile/lib/modules/backup/ui/album_info_list_tile.dart @@ -1,10 +1,9 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -25,14 +24,13 @@ class AlbumInfoListTile extends HookConsumerWidget { ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo); ColorFilter selectedFilter = ColorFilter.mode( - Theme.of(context).primaryColor.withAlpha(100), + context.primaryColor.withAlpha(100), BlendMode.darken, ); ColorFilter excludedFilter = ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken); ColorFilter unselectedFilter = const ColorFilter.mode(Colors.black, BlendMode.color); - var isDarkTheme = Theme.of(context).brightness == Brightness.dark; var assetCount = useState(0); @@ -56,11 +54,11 @@ class AlbumInfoListTile extends HookConsumerWidget { buildTileColor() { if (isSelected) { - return isDarkTheme - ? Theme.of(context).primaryColor.withAlpha(100) - : Theme.of(context).primaryColor.withAlpha(25); + return context.isDarkTheme + ? context.primaryColor.withAlpha(100) + : context.primaryColor.withAlpha(25); } else if (isExcluded) { - return isDarkTheme + return context.isDarkTheme ? Colors.red[300]?.withAlpha(150) : Colors.red[100]?.withAlpha(150); } else { @@ -75,23 +73,10 @@ class AlbumInfoListTile extends HookConsumerWidget { if (isExcluded) { // Remove from exclude album list ref - .watch(backupProvider.notifier) + .read(backupProvider.notifier) .removeExcludedAlbumForBackup(albumInfo); } else { // Add to exclude album list - if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 && - ref - .watch(backupProvider) - .selectedBackupAlbums - .contains(albumInfo)) { - ImmichToast.show( - context: context, - msg: "backup_err_only_album".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } if (albumInfo.id == 'isAll' || albumInfo.name == 'Recents') { ImmichToast.show( @@ -104,7 +89,7 @@ class AlbumInfoListTile extends HookConsumerWidget { } ref - .watch(backupProvider.notifier) + .read(backupProvider.notifier) .addExcludedAlbumForBackup(albumInfo); } }, @@ -114,19 +99,9 @@ class AlbumInfoListTile extends HookConsumerWidget { onTap: () { HapticFeedback.selectionClick(); if (isSelected) { - if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) { - ImmichToast.show( - context: context, - msg: "backup_err_only_album".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.watch(backupProvider.notifier).removeAlbumForBackup(albumInfo); + ref.read(backupProvider.notifier).removeAlbumForBackup(albumInfo); } else { - ref.watch(backupProvider.notifier).addAlbumForBackup(albumInfo); + ref.read(backupProvider.notifier).addAlbumForBackup(albumInfo); } }, leading: ClipRRect( @@ -159,13 +134,13 @@ class AlbumInfoListTile extends HookConsumerWidget { subtitle: Text(assetCount.value.toString()), trailing: IconButton( onPressed: () { - AutoRouter.of(context).push( + context.autoPush( AlbumPreviewRoute(album: albumInfo.albumEntity), ); }, icon: Icon( Icons.image_outlined, - color: Theme.of(context).primaryColor, + color: context.primaryColor, size: 24, ), splashRadius: 25, diff --git a/mobile/lib/modules/backup/ui/backup_info_card.dart b/mobile/lib/modules/backup/ui/backup_info_card.dart index bf52c79e6..4feccd19b 100644 --- a/mobile/lib/modules/backup/ui/backup_info_card.dart +++ b/mobile/lib/modules/backup/ui/backup_info_card.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; class BackupInfoCard extends StatelessWidget { final String title; @@ -14,13 +15,11 @@ class BackupInfoCard extends StatelessWidget { @override Widget build(BuildContext context) { - var isDarkMode = Theme.of(context).brightness == Brightness.dark; - return Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), // if you need this side: BorderSide( - color: isDarkMode + color: context.isDarkTheme ? const Color.fromARGB(255, 56, 56, 56) : Colors.black12, width: 1, @@ -29,17 +28,17 @@ class BackupInfoCard extends StatelessWidget { elevation: 0, borderOnForeground: false, child: ListTile( - minVerticalPadding: 15, + minVerticalPadding: 18, isThreeLine: true, title: Text( title, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + style: context.textTheme.titleMedium, ), subtitle: Padding( - padding: const EdgeInsets.only(top: 8.0), + padding: const EdgeInsets.only(top: 4.0, right: 18.0), child: Text( subtitle, - style: const TextStyle(fontSize: 12), + style: context.textTheme.bodyMedium, ), ), trailing: Column( @@ -47,9 +46,12 @@ class BackupInfoCard extends StatelessWidget { children: [ Text( info, - style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + style: context.textTheme.titleLarge, ), - const Text("backup_info_card_assets").tr(), + Text( + "backup_info_card_assets", + style: context.textTheme.labelLarge, + ).tr(), ], ), ), diff --git a/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart b/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart index 23bdf11ed..926ccd25b 100644 --- a/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart +++ b/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart @@ -1,9 +1,9 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/backup/models/backup_state.model.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart'; @@ -53,7 +53,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { ), backgroundColor: Colors.white, onPressed: () { - AutoRouter.of(context).push(const FailedBackupStatusRoute()); + context.autoPush(const FailedBackupStatusRoute()); }, ); } @@ -61,7 +61,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { Widget buildAssetInfoTable() { return Table( border: TableBorder.all( - color: Theme.of(context).primaryColorLight, + color: context.themeData.primaryColorLight, width: 1, ), children: [ @@ -176,7 +176,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { onTap: () => isShowThumbnail.value = true, child: Icon( Icons.image_outlined, - color: Theme.of(context).primaryColor, + color: context.primaryColor, size: 30, ), ), @@ -188,9 +188,9 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( "backup_controller_page_uploading_file_info", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + style: context.textTheme.titleSmall, ).tr(), if (ref.watch(errorBackupListProvider).isNotEmpty) buildErrorChip(), ], @@ -206,7 +206,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { minHeight: 10.0, value: uploadProgress / 100.0, backgroundColor: Colors.grey, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), Text( diff --git a/mobile/lib/modules/backup/ui/ios_debug_info_tile.dart b/mobile/lib/modules/backup/ui/ios_debug_info_tile.dart index 9ab92104e..283c329b6 100644 --- a/mobile/lib/modules/backup/ui/ios_debug_info_tile.dart +++ b/mobile/lib/modules/backup/ui/ios_debug_info_tile.dart @@ -1,5 +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/modules/backup/providers/ios_background_settings.provider.dart'; import 'package:intl/intl.dart'; @@ -43,7 +44,7 @@ class IosDebugInfoTile extends HookConsumerWidget { style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), subtitle: Text( @@ -54,7 +55,7 @@ class IosDebugInfoTile extends HookConsumerWidget { ), leading: Icon( Icons.bug_report, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ); } diff --git a/mobile/lib/modules/backup/views/album_preview_page.dart b/mobile/lib/modules/backup/views/album_preview_page.dart index 27ca79082..cdb0204ec 100644 --- a/mobile/lib/modules/backup/views/album_preview_page.dart +++ b/mobile/lib/modules/backup/views/album_preview_page.dart @@ -1,9 +1,9 @@ import 'dart:typed_data'; -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -53,7 +53,7 @@ class AlbumPreviewPage extends HookConsumerWidget { ], ), leading: IconButton( - onPressed: () => AutoRouter.of(context).pop(), + onPressed: () => context.autoPop(), icon: const Icon(Icons.arrow_back_ios_new_rounded), ), ), diff --git a/mobile/lib/modules/backup/views/backup_album_selection_page.dart b/mobile/lib/modules/backup/views/backup_album_selection_page.dart index 9e4118038..96ed879ac 100644 --- a/mobile/lib/modules/backup/views/backup_album_selection_page.dart +++ b/mobile/lib/modules/backup/views/backup_album_selection_page.dart @@ -1,15 +1,13 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/ui/album_info_card.dart'; import 'package:immich_mobile/modules/backup/ui/album_info_list_tile.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; -import 'package:immich_mobile/shared/ui/immich_toast.dart'; class BackupAlbumSelectionPage extends HookConsumerWidget { const BackupAlbumSelectionPage({Key? key}) : super(key: key); @@ -18,7 +16,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { // final availableAlbums = ref.watch(backupProvider).availableAlbums; final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; - final isDarkTheme = Theme.of(context).brightness == Brightness.dark; + final isDarkTheme = context.isDarkTheme; final allAlbums = ref.watch(backupProvider).availableAlbums; // Albums which are displayed to the user @@ -91,19 +89,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { buildSelectedAlbumNameChip() { return selectedBackupAlbums.map((album) { - void removeSelection() { - if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) { - ImmichToast.show( - context: context, - msg: "backup_err_only_album".tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.watch(backupProvider.notifier).removeAlbumForBackup(album); - } + void removeSelection() => + ref.read(backupProvider.notifier).removeAlbumForBackup(album); return Padding( padding: const EdgeInsets.only(right: 8.0), @@ -113,12 +100,12 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { label: Text( album.name, style: TextStyle( - fontSize: 10, + fontSize: 12, color: isDarkTheme ? Colors.black : Colors.white, fontWeight: FontWeight.bold, ), ), - backgroundColor: Theme.of(context).primaryColor, + backgroundColor: context.primaryColor, deleteIconColor: isDarkTheme ? Colors.black : Colors.white, deleteIcon: const Icon( Icons.cancel_rounded, @@ -147,7 +134,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { label: Text( album.name, style: TextStyle( - fontSize: 10, + fontSize: 12, color: isDarkTheme ? Colors.black : immichBackgroundColor, fontWeight: FontWeight.bold, ), @@ -211,12 +198,11 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( leading: IconButton( - onPressed: () => AutoRouter.of(context).pop(), + onPressed: () => context.autoPop(), icon: const Icon(Icons.arrow_back_ios_rounded), ), title: const Text( "backup_album_selection_page_select_albums", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ).tr(), elevation: 0, ), @@ -232,12 +218,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { vertical: 8.0, horizontal: 16.0, ), - child: const Text( + child: Text( "backup_album_selection_page_selection_info", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), + style: context.textTheme.titleSmall, ).tr(), ), // Selected Album Chips @@ -252,47 +235,6 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ), ), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), - child: Card( - margin: const EdgeInsets.all(0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - side: BorderSide( - color: isDarkTheme - ? const Color.fromARGB(255, 0, 0, 0) - : const Color.fromARGB(255, 235, 235, 235), - width: 1, - ), - ), - elevation: 0, - borderOnForeground: false, - child: Column( - children: [ - ListTile( - visualDensity: VisualDensity.compact, - title: const Text( - "backup_album_selection_page_total_assets", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ).tr(), - trailing: Text( - ref - .watch(backupProvider) - .allUniqueAssets - .length - .toString(), - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - ], - ), - ), - ), - ListTile( title: Text( "backup_album_selection_page_albums_device".tr( @@ -304,19 +246,14 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { .toString(), ], ), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), + style: context.textTheme.titleSmall, ), subtitle: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Text( "backup_album_selection_page_albums_tap", - style: TextStyle( - fontSize: 12, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, + style: context.textTheme.labelLarge?.copyWith( + color: context.primaryColor, ), ).tr(), ), @@ -325,7 +262,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { icon: Icon( Icons.info, size: 20, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), onPressed: () { // show the dialog @@ -342,7 +279,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ).tr(), content: SingleChildScrollView( diff --git a/mobile/lib/modules/backup/views/backup_controller_page.dart b/mobile/lib/modules/backup/views/backup_controller_page.dart index 9dc40eced..2bdb3a5dd 100644 --- a/mobile/lib/modules/backup/views/backup_controller_page.dart +++ b/mobile/lib/modules/backup/views/backup_controller_page.dart @@ -1,11 +1,12 @@ import 'dart:io'; -import 'package:auto_route/auto_route.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/backup/background_service/background.service.dart'; import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart'; import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart'; @@ -38,6 +39,7 @@ class BackupControllerPage extends HookConsumerWidget { final settingsService = ref.watch(appSettingsServiceProvider); final showBackupFix = Platform.isAndroid && settingsService.getSetting(AppSettingsEnum.advancedTroubleshooting); + final hasAnyAlbum = backupState.selectedBackupAlbums.isNotEmpty; final appRefreshDisabled = Platform.isIOS && settings?.appRefreshEnabled != true; @@ -49,7 +51,6 @@ class BackupControllerPage extends HookConsumerWidget { !hasExclusiveAccess ? false : true; - var isDarkMode = Theme.of(context).brightness == Brightness.dark; final checkInProgress = useState(false); useEffect( @@ -151,7 +152,7 @@ class BackupControllerPage extends HookConsumerWidget { return ListTile( leading: Icon( Icons.warning_rounded, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), title: const Text( "Check for corrupt asset backups", @@ -187,12 +188,12 @@ class BackupControllerPage extends HookConsumerWidget { leading: isAutoBackup ? Icon( Icons.cloud_done_rounded, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ) : const Icon(Icons.cloud_off_rounded), title: Text( backUpOption, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + style: context.textTheme.titleSmall, ), subtitle: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), @@ -212,9 +213,8 @@ class BackupControllerPage extends HookConsumerWidget { .setAutoBackup(!isAutoBackup), child: Text( backupBtnText, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, + style: context.textTheme.labelLarge?.copyWith( + color: context.isDarkTheme ? Colors.black : Colors.white, ), ), ), @@ -229,6 +229,9 @@ class BackupControllerPage extends HookConsumerWidget { final snackBar = SnackBar( content: Text( msg.tr(), + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), ), backgroundColor: Colors.red, ); @@ -266,7 +269,7 @@ class BackupControllerPage extends HookConsumerWidget { style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12), ).tr(), onPressed: () { - Navigator.of(context).pop(); + context.pop(); }, ), ], @@ -279,7 +282,7 @@ class BackupControllerPage extends HookConsumerWidget { final bool isBackgroundEnabled = backupState.backgroundBackup; final bool isWifiRequired = backupState.backupRequireWifi; final bool isChargingRequired = backupState.backupRequireCharging; - final Color activeColor = Theme.of(context).primaryColor; + final Color activeColor = context.primaryColor; String formatBackupDelaySliderValue(double v) { if (v == 0.0) { @@ -334,7 +337,7 @@ class BackupControllerPage extends HookConsumerWidget { isBackgroundEnabled ? "backup_controller_page_background_is_on" : "backup_controller_page_background_is_off", - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + style: context.textTheme.titleSmall, ).tr(), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -410,7 +413,7 @@ class BackupControllerPage extends HookConsumerWidget { max: 3.0, divisions: 3, label: formatBackupDelaySliderValue(triggerDelay.value), - activeColor: Theme.of(context).primaryColor, + activeColor: context.primaryColor, ), ), ElevatedButton( @@ -425,9 +428,8 @@ class BackupControllerPage extends HookConsumerWidget { isBackgroundEnabled ? "backup_controller_page_background_turn_off" : "backup_controller_page_background_turn_on", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, + style: context.textTheme.labelLarge?.copyWith( + color: context.isDarkTheme ? Colors.black : Colors.white, ), ).tr(), ), @@ -510,10 +512,8 @@ class BackupControllerPage extends HookConsumerWidget { padding: const EdgeInsets.only(top: 8.0), child: Text( text.trim().substring(0, text.length - 2), - style: TextStyle( - color: Theme.of(context).primaryColor, - fontSize: 12, - fontWeight: FontWeight.bold, + style: context.textTheme.labelLarge?.copyWith( + color: context.primaryColor, ), ), ); @@ -522,10 +522,8 @@ class BackupControllerPage extends HookConsumerWidget { padding: const EdgeInsets.only(top: 8.0), child: Text( "backup_controller_page_none_selected".tr(), - style: TextStyle( - color: Theme.of(context).primaryColor, - fontSize: 12, - fontWeight: FontWeight.bold, + style: context.textTheme.labelLarge?.copyWith( + color: context.primaryColor, ), ), ); @@ -545,10 +543,8 @@ class BackupControllerPage extends HookConsumerWidget { padding: const EdgeInsets.only(top: 8.0), child: Text( text.trim().substring(0, text.length - 2), - style: TextStyle( + style: context.textTheme.labelLarge?.copyWith( color: Colors.red[300], - fontSize: 12, - fontWeight: FontWeight.bold, ), ), ); @@ -558,49 +554,57 @@ class BackupControllerPage extends HookConsumerWidget { } buildFolderSelectionTile() { - return Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - side: BorderSide( - color: isDarkMode - ? const Color.fromARGB(255, 56, 56, 56) - : Colors.black12, - width: 1, - ), - ), - elevation: 0, - borderOnForeground: false, - child: ListTile( - minVerticalPadding: 15, - title: const Text( - "backup_controller_page_albums", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), - ).tr(), - subtitle: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "backup_controller_page_to_backup", - style: TextStyle(fontSize: 12), - ).tr(), - buildSelectedAlbumName(), - buildExcludedAlbumName(), - ], + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: context.isDarkTheme + ? const Color.fromARGB(255, 56, 56, 56) + : Colors.black12, + width: 1, ), ), - trailing: ElevatedButton( - onPressed: () { - AutoRouter.of(context).push(const BackupAlbumSelectionRoute()); - }, - child: const Text( - "backup_controller_page_select", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - ), + elevation: 0, + borderOnForeground: false, + child: ListTile( + minVerticalPadding: 18, + title: Text( + "backup_controller_page_albums", + style: context.textTheme.titleMedium, ).tr(), + subtitle: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "backup_controller_page_to_backup", + style: context.textTheme.bodyMedium, + ).tr(), + buildSelectedAlbumName(), + buildExcludedAlbumName(), + ], + ), + ), + trailing: ElevatedButton( + onPressed: () async { + await context.autoPush(const BackupAlbumSelectionRoute()); + // waited until returning from selection + await ref + .read(backupProvider.notifier) + .backupAlbumSelectionDone(); + // waited until backup albums are stored in DB + ref.read(albumProvider.notifier).getDeviceAlbums(); + }, + child: const Text( + "backup_controller_page_select", + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ).tr(), + ), ), ), ); @@ -650,7 +654,7 @@ class BackupControllerPage extends HookConsumerWidget { child: const Text( "backup_controller_page_start_backup", style: TextStyle( - fontSize: 14, + fontSize: 16, fontWeight: FontWeight.bold, ), ).tr(), @@ -673,12 +677,11 @@ class BackupControllerPage extends HookConsumerWidget { elevation: 0, title: const Text( "backup_controller_page_backup", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ).tr(), leading: IconButton( onPressed: () { ref.watch(websocketProvider.notifier).listenUploadEvent(); - AutoRouter.of(context).pop(true); + context.autoPop(true); }, splashRadius: 24, icon: const Icon( @@ -690,55 +693,49 @@ class BackupControllerPage extends HookConsumerWidget { padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32), child: ListView( // crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: const Text( - "backup_controller_page_info", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ).tr(), - ), - buildFolderSelectionTile(), - BackupInfoCard( - title: "backup_controller_page_total".tr(), - subtitle: "backup_controller_page_total_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.allUniqueAssets.length}", - ), - BackupInfoCard( - title: "backup_controller_page_backup".tr(), - subtitle: "backup_controller_page_backup_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.selectedAlbumsBackupAssetsIds.length}", - ), - BackupInfoCard( - title: "backup_controller_page_remainder".tr(), - subtitle: "backup_controller_page_remainder_sub".tr(), - info: ref.watch(backupProvider).availableAlbums.isEmpty - ? "..." - : "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}", - ), - const Divider(), - buildAutoBackupController(), - const Divider(), - AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: Platform.isIOS - ? (appRefreshDisabled - ? buildBackgroundAppRefreshWarning() - : buildBackgroundBackupController()) - : buildBackgroundBackupController(), - ), - if (showBackupFix) const Divider(), - if (showBackupFix) buildCheckCorruptBackups(), - const Divider(), - const Divider(), - const CurrentUploadingAssetInfoBox(), - if (!hasExclusiveAccess) buildBackgroundBackupInfo(), - buildBackupButton(), - ], + children: hasAnyAlbum + ? [ + buildFolderSelectionTile(), + BackupInfoCard( + title: "backup_controller_page_total".tr(), + subtitle: "backup_controller_page_total_sub".tr(), + info: ref.watch(backupProvider).availableAlbums.isEmpty + ? "..." + : "${backupState.allUniqueAssets.length}", + ), + BackupInfoCard( + title: "backup_controller_page_backup".tr(), + subtitle: "backup_controller_page_backup_sub".tr(), + info: ref.watch(backupProvider).availableAlbums.isEmpty + ? "..." + : "${backupState.selectedAlbumsBackupAssetsIds.length}", + ), + BackupInfoCard( + title: "backup_controller_page_remainder".tr(), + subtitle: "backup_controller_page_remainder_sub".tr(), + info: ref.watch(backupProvider).availableAlbums.isEmpty + ? "..." + : "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}", + ), + const Divider(), + buildAutoBackupController(), + const Divider(), + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + child: Platform.isIOS + ? (appRefreshDisabled + ? buildBackgroundAppRefreshWarning() + : buildBackgroundBackupController()) + : buildBackgroundBackupController(), + ), + if (showBackupFix) const Divider(), + if (showBackupFix) buildCheckCorruptBackups(), + const Divider(), + const CurrentUploadingAssetInfoBox(), + if (!hasExclusiveAccess) buildBackgroundBackupInfo(), + buildBackupButton(), + ] + : [buildFolderSelectionTile()], ), ), ); diff --git a/mobile/lib/modules/backup/views/failed_backup_status_page.dart b/mobile/lib/modules/backup/views/failed_backup_status_page.dart index c55383cf3..433ed3420 100644 --- a/mobile/lib/modules/backup/views/failed_backup_status_page.dart +++ b/mobile/lib/modules/backup/views/failed_backup_status_page.dart @@ -1,6 +1,6 @@ -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/modules/backup/providers/error_backup_list.provider.dart'; import 'package:intl/intl.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -20,7 +20,7 @@ class FailedBackupStatusPage extends HookConsumerWidget { ), leading: IconButton( onPressed: () { - AutoRouter.of(context).pop(true); + context.autoPop(true); }, splashRadius: 24, icon: const Icon( @@ -114,7 +114,7 @@ class FailedBackupStatusPage extends HookConsumerWidget { style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), ), diff --git a/mobile/lib/modules/favorite/providers/favorite_provider.dart b/mobile/lib/modules/favorite/providers/favorite_provider.dart index 427d2c88b..0da6b3f8a 100644 --- a/mobile/lib/modules/favorite/providers/favorite_provider.dart +++ b/mobile/lib/modules/favorite/providers/favorite_provider.dart @@ -1,15 +1,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart'; +import 'package:immich_mobile/utils/renderlist_generator.dart'; import 'package:isar/isar.dart'; -final favoriteAssetsProvider = StreamProvider((ref) async* { +final favoriteAssetsProvider = StreamProvider((ref) { final user = ref.watch(currentUserProvider); - if (user == null) return; + if (user == null) return const Stream.empty(); final query = ref .watch(dbProvider) .assets @@ -19,11 +18,5 @@ final favoriteAssetsProvider = StreamProvider((ref) async* { .isFavoriteEqualTo(true) .isTrashedEqualTo(false) .sortByFileCreatedAt(); - final settings = ref.watch(appSettingsServiceProvider); - final groupBy = - GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - yield await RenderList.fromQuery(query, groupBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupBy); - } + return renderListGenerator(query, ref); }); diff --git a/mobile/lib/modules/favorite/views/favorites_page.dart b/mobile/lib/modules/favorite/views/favorites_page.dart index 62f8763bb..095297507 100644 --- a/mobile/lib/modules/favorite/views/favorites_page.dart +++ b/mobile/lib/modules/favorite/views/favorites_page.dart @@ -1,8 +1,9 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/shared/models/asset.dart'; @@ -28,7 +29,7 @@ class FavoritesPage extends HookConsumerWidget { AppBar buildAppBar() { return AppBar( leading: IconButton( - onPressed: () => AutoRouter.of(context).pop(), + onPressed: () => context.autoPop(), icon: const Icon(Icons.arrow_back_ios_rounded), ), centerTitle: true, @@ -62,22 +63,18 @@ class FavoritesPage extends HookConsumerWidget { child: SizedBox( height: 64, child: Card( - child: Column( - children: [ - ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - leading: const Icon( - Icons.star_border, - ), - title: const Text( - "Unfavorite", - style: TextStyle(fontSize: 14), - ), - onTap: processing.value ? null : unfavorite, - ), - ], + child: ListTile( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + leading: const Icon( + Icons.star_border, + ), + title: const Text( + "Unfavorite", + style: TextStyle(fontSize: 14), + ), + onTap: processing.value ? null : unfavorite, ), ), ), @@ -87,10 +84,8 @@ class FavoritesPage extends HookConsumerWidget { return Scaffold( appBar: buildAppBar(), - body: ref.watch(favoriteAssetsProvider).when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stackTrace) => Center(child: Text(error.toString())), - data: (data) => data.isEmpty + body: ref.watch(favoriteAssetsProvider).widgetWhen( + onData: (data) => data.isEmpty ? Center( child: Text('favorites_page_no_favorites'.tr()), ) diff --git a/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart b/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart index d63b0631e..1455f53ef 100644 --- a/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart +++ b/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart @@ -1,8 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.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/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; -class GroupDividerTitle extends ConsumerWidget { +class GroupDividerTitle extends HookConsumerWidget { const GroupDividerTitle({ Key? key, required this.text, @@ -20,6 +25,18 @@ class GroupDividerTitle extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final appSettingService = ref.watch(appSettingsServiceProvider); + final groupBy = useState(GroupAssetsBy.day); + + useEffect( + () { + groupBy.value = GroupAssetsBy.values[ + appSettingService.getSetting(AppSettingsEnum.groupAssetsBy)]; + return null; + }, + [], + ); + void handleTitleIconClick() { HapticFeedback.heavyImpact(); if (selected) { @@ -30,8 +47,8 @@ class GroupDividerTitle extends ConsumerWidget { } return Padding( - padding: const EdgeInsets.only( - top: 12.0, + padding: EdgeInsets.only( + top: groupBy.value == GroupAssetsBy.month ? 32.0 : 16.0, bottom: 16.0, left: 12.0, right: 12.0, @@ -40,10 +57,14 @@ class GroupDividerTitle extends ConsumerWidget { children: [ Text( text, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - ), + style: groupBy.value == GroupAssetsBy.month + ? context.textTheme.bodyLarge?.copyWith( + fontSize: 24.0, + ) + : context.textTheme.labelLarge?.copyWith( + color: context.textTheme.labelLarge?.color?.withAlpha(250), + fontWeight: FontWeight.w500, + ), ), const Spacer(), GestureDetector( @@ -51,7 +72,7 @@ class GroupDividerTitle extends ConsumerWidget { child: multiselectEnabled && selected ? Icon( Icons.check_circle_rounded, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ) : const Icon( Icons.check_circle_outline_rounded, diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart index 2c0f63394..562b7892c 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -5,13 +5,13 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid_view.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; class ImmichAssetGrid extends HookConsumerWidget { @@ -130,12 +130,8 @@ class ImmichAssetGrid extends HookConsumerWidget { if (renderList != null) return buildAssetGridView(renderList!); final renderListFuture = ref.watch(renderListProvider(assets!)); - return renderListFuture.when( - data: (renderList) => buildAssetGridView(renderList), - error: (err, stack) => Center(child: Text("$err")), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), + return renderListFuture.widgetWhen( + onData: (renderList) => buildAssetGridView(renderList), ); } } diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart index 4424f722e..e27292eaa 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart @@ -4,10 +4,11 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart'; import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/utils/builtin_extensions.dart'; +import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'asset_grid_data_structure.dart'; import 'group_divider_title.dart'; @@ -223,10 +224,9 @@ class ImmichAssetGridViewState extends State { padding: const EdgeInsets.only(left: 12.0, top: 24.0), child: Text( title, - style: TextStyle( + style: const TextStyle( fontSize: 26, - fontWeight: FontWeight.bold, - color: Theme.of(context).textTheme.displayLarge?.color, + fontWeight: FontWeight.w500, ), ), ); @@ -374,7 +374,7 @@ class ImmichAssetGridViewState extends State { scrollStateListener: dragScrolling, itemPositionsListener: _itemPositionsListener, controller: _itemScrollController, - backgroundColor: Theme.of(context).hintColor, + backgroundColor: context.themeData.hintColor, labelTextBuilder: _labelBuilder, labelConstraints: const BoxConstraints(maxHeight: 28), scrollbarAnimationDuration: const Duration(milliseconds: 300), diff --git a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart index 16423b3b4..694279c0d 100644 --- a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart @@ -1,6 +1,6 @@ -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; @@ -43,9 +43,9 @@ class ThumbnailImage extends StatelessWidget { @override Widget build(BuildContext context) { - final isDarkTheme = Theme.of(context).brightness == Brightness.dark; - final assetContainerColor = - isDarkTheme ? Colors.blueGrey : Theme.of(context).primaryColorLight; + final assetContainerColor = context.isDarkTheme + ? Colors.blueGrey + : context.themeData.primaryColorLight; // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id final isFromDto = asset.id == Isar.autoIncrement; @@ -58,7 +58,7 @@ class ThumbnailImage extends StatelessWidget { ), child: Icon( Icons.check_circle_rounded, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ); } else { @@ -178,7 +178,7 @@ class ThumbnailImage extends StatelessWidget { onSelect?.call(); } } else { - AutoRouter.of(context).push( + context.autoPush( GalleryViewerRoute( initialIndex: index, loadAsset: loadAsset, @@ -197,7 +197,9 @@ class ThumbnailImage extends StatelessWidget { }, child: Stack( children: [ - Container( + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.decelerate, decoration: BoxDecoration( border: multiselectEnabled && isSelected ? Border.all( diff --git a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart index 4f6e2706d..8ae7f98cd 100644 --- a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart +++ b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart @@ -1,6 +1,7 @@ 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/modules/album/ui/add_to_album_sliverlist.dart'; import 'package:immich_mobile/modules/home/models/selection_state.dart'; import 'package:immich_mobile/modules/home/ui/delete_dialog.dart'; @@ -42,7 +43,6 @@ class ControlBottomAppBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - var isDarkMode = Theme.of(context).brightness == Brightness.dark; var hasRemote = selectionAssetState.hasRemote || selectionAssetState.hasMerged; var hasLocal = selectionAssetState.hasLocal; @@ -128,7 +128,7 @@ class ControlBottomAppBar extends ConsumerWidget { ScrollController scrollController, ) { return Card( - color: isDarkMode ? Colors.grey[900] : Colors.grey[100], + color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[100], surfaceTintColor: Colors.transparent, elevation: 18.0, shape: const RoundedRectangleBorder( @@ -150,6 +150,7 @@ class ControlBottomAppBar extends ConsumerWidget { SizedBox( height: 70, child: ListView( + shrinkWrap: true, scrollDirection: Axis.horizontal, children: renderActionButtons(), ), @@ -211,12 +212,12 @@ class AddToAlbumTitleRow extends StatelessWidget { onPressed: onCreateNewAlbum, icon: Icon( Icons.add, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), label: Text( "common_create_new_album", style: TextStyle( - color: Theme.of(context).primaryColor, + color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14, ), diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index d41022a29..58770ed5c 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -1,12 +1,12 @@ import 'dart:async'; -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; @@ -28,6 +28,7 @@ import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/ui/immich_app_bar.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; +import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/utils/selection_handlers.dart'; class HomePage extends HookConsumerWidget { @@ -44,17 +45,19 @@ class HomePage extends HookConsumerWidget { final sharedAlbums = ref.watch(sharedAlbumProvider); final albumService = ref.watch(albumServiceProvider); final currentUser = ref.watch(currentUserProvider); + final timelineUsers = ref.watch(timelineUsersIdsProvider); final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); final tipOneOpacity = useState(0.0); final refreshCount = useState(0); - final processing = useState(false); + final processing = useProcessingOverlay(); useEffect( () { ref.read(websocketProvider.notifier).connect(); Future(() => ref.read(assetProvider.notifier).getAllAsset()); + ref.read(assetProvider.notifier).getPartnerAssets(); ref.read(albumProvider.notifier).getAllAlbums(); ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); ref.read(serverInfoProvider.notifier).getServerInfo(); @@ -84,30 +87,65 @@ class HomePage extends HookConsumerWidget { SelectionAssetState.fromSelection(selectedAssets); } - List remoteOnlySelection({String? localErrorMessage}) { - final Set assets = selection.value; + errorBuilder(String? msg) => msg != null && msg.isNotEmpty + ? () => ImmichToast.show( + context: context, + msg: msg, + gravity: ToastGravity.BOTTOM, + ) + : null; + + Iterable remoteOnly( + Iterable assets, { + void Function()? errorCallback, + }) { final bool onlyRemote = assets.every((e) => e.isRemote); if (!onlyRemote) { - if (localErrorMessage != null && localErrorMessage.isNotEmpty) { - ImmichToast.show( - context: context, - msg: localErrorMessage, - gravity: ToastGravity.BOTTOM, - ); - } - return assets.where((a) => a.isRemote).toList(); + if (errorCallback != null) errorCallback(); + return assets.where((a) => a.isRemote); } - return assets.toList(); + return assets; } + Iterable ownedOnly( + Iterable assets, { + void Function()? errorCallback, + }) { + if (currentUser == null) return []; + final userId = currentUser.isarId; + final bool onlyOwned = assets.every((e) => e.ownerId == userId); + if (!onlyOwned) { + if (errorCallback != null) errorCallback(); + return assets.where((a) => a.ownerId == userId); + } + return assets; + } + + Iterable ownedRemoteSelection({ + String? localErrorMessage, + String? ownerErrorMessage, + }) { + final assets = selection.value; + return remoteOnly( + ownedOnly(assets, errorCallback: errorBuilder(ownerErrorMessage)), + errorCallback: errorBuilder(localErrorMessage), + ); + } + + Iterable remoteSelection({String? errorMessage}) => remoteOnly( + selection.value, + errorCallback: errorBuilder(errorMessage), + ); + void onShareAssets(bool shareLocal) { processing.value = true; if (shareLocal) { handleShareAssets(ref, context, selection.value.toList()); } else { - final ids = remoteOnlySelection().map((e) => e.remoteId!); - AutoRouter.of(context) - .push(SharedLinkEditRoute(assetsList: ids.toList())); + final ids = + remoteSelection(errorMessage: "home_page_share_err_local".tr()) + .map((e) => e.remoteId!); + context.autoPush(SharedLinkEditRoute(assetsList: ids.toList())); } processing.value = false; selectionEnabledHook.value = false; @@ -116,11 +154,12 @@ class HomePage extends HookConsumerWidget { void onFavoriteAssets() async { processing.value = true; try { - final remoteAssets = remoteOnlySelection( + final remoteAssets = ownedRemoteSelection( localErrorMessage: 'home_page_favorite_err_local'.tr(), + ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), ); if (remoteAssets.isNotEmpty) { - await handleFavoriteAssets(ref, context, remoteAssets); + await handleFavoriteAssets(ref, context, remoteAssets.toList()); } } finally { processing.value = false; @@ -131,10 +170,11 @@ class HomePage extends HookConsumerWidget { void onArchiveAsset() async { processing.value = true; try { - final remoteAssets = remoteOnlySelection( + final remoteAssets = ownedRemoteSelection( localErrorMessage: 'home_page_archive_err_local'.tr(), + ownerErrorMessage: 'home_page_archive_err_partner'.tr(), ); - await handleArchiveAssets(ref, context, remoteAssets); + await handleArchiveAssets(ref, context, remoteAssets.toList()); } finally { processing.value = false; selectionEnabledHook.value = false; @@ -144,12 +184,16 @@ class HomePage extends HookConsumerWidget { void onDelete() async { processing.value = true; try { + final toDelete = ownedOnly( + selection.value, + errorCallback: errorBuilder('home_page_delete_err_partner'.tr()), + ).toList(); await ref .read(assetProvider.notifier) - .deleteAssets(selection.value, force: !trashEnabled); + .deleteAssets(toDelete, force: !trashEnabled); - final hasRemote = selection.value.any((a) => a.isRemote); - final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset'; + final hasRemote = toDelete.any((a) => a.isRemote); + final assetOrAssets = toDelete.length > 1 ? 'assets' : 'asset'; final trashOrRemoved = !trashEnabled ? 'deleted permanently' : 'trashed'; if (hasRemote) { @@ -169,10 +213,10 @@ class HomePage extends HookConsumerWidget { processing.value = true; selectionEnabledHook.value = false; try { - ref.read(manualUploadProvider.notifier).uploadAssets( - context, - selection.value.where((a) => a.storage == AssetState.local), - ); + ref.read(manualUploadProvider.notifier).uploadAssets( + context, + selection.value.where((a) => a.storage == AssetState.local), + ); } finally { processing.value = false; } @@ -181,8 +225,8 @@ class HomePage extends HookConsumerWidget { void onAddToAlbum(Album album) async { processing.value = true; try { - final Iterable assets = remoteOnlySelection( - localErrorMessage: "home_page_add_to_album_err_local".tr(), + final Iterable assets = remoteSelection( + errorMessage: "home_page_add_to_album_err_local".tr(), ); if (assets.isEmpty) { return; @@ -229,8 +273,8 @@ class HomePage extends HookConsumerWidget { void onCreateNewAlbum() async { processing.value = true; try { - final Iterable assets = remoteOnlySelection( - localErrorMessage: "home_page_add_to_album_err_local".tr(), + final Iterable assets = remoteSelection( + errorMessage: "home_page_add_to_album_err_local".tr(), ); if (assets.isEmpty) { return; @@ -243,7 +287,7 @@ class HomePage extends HookConsumerWidget { ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums(); selectionEnabledHook.value = false; - AutoRouter.of(context).push(AlbumViewerRoute(albumId: result.id)); + context.autoPush(AlbumViewerRoute(albumId: result.id)); } } finally { processing.value = false; @@ -271,22 +315,21 @@ class HomePage extends HookConsumerWidget { Future refreshAssets() async { final fullRefresh = refreshCount.value > 0; await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh); + if (timelineUsers.length > 1) { + await ref.read(assetProvider.notifier).getPartnerAssets(); + } if (fullRefresh) { // refresh was forced: user requested another refresh within 2 seconds refreshCount.value = 0; } else { refreshCount.value++; // set counter back to 0 if user does not request refresh again - Timer(const Duration(seconds: 4), () { - refreshCount.value = 0; - }); + Timer(const Duration(seconds: 4), () => refreshCount.value = 0); } } buildLoadingIndicator() { - Timer(const Duration(seconds: 2), () { - tipOneOpacity.value = 1; - }); + Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1); return Center( child: Column( @@ -300,7 +343,7 @@ class HomePage extends HookConsumerWidget { style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ).tr(), ), @@ -331,7 +374,13 @@ class HomePage extends HookConsumerWidget { bottom: false, child: Stack( children: [ - ref.watch(assetsProvider(currentUser?.isarId)).when( + ref + .watch( + timelineUsers.length > 1 + ? multiUserAssetsProvider(timelineUsers) + : assetsProvider(currentUser?.isarId), + ) + .when( data: (data) => data.isEmpty ? buildLoadingIndicator() : ImmichAssetGrid( @@ -339,11 +388,10 @@ class HomePage extends HookConsumerWidget { listener: selectionListener, selectionActive: selectionEnabledHook.value, onRefresh: refreshAssets, - topWidget: (currentUser != null && - currentUser.memoryEnabled != null && - currentUser.memoryEnabled!) - ? const MemoryLane() - : const SizedBox(), + topWidget: + (currentUser != null && currentUser.memoryEnabled) + ? const MemoryLane() + : const SizedBox(), showStack: true, ), error: (error, _) => Center(child: Text(error.toString())), @@ -364,7 +412,6 @@ class HomePage extends HookConsumerWidget { selectionAssetState: selectionAssetState.value, onStack: onStack, ), - if (processing.value) const Center(child: ImmichLoadingIndicator()), ], ), ); diff --git a/mobile/lib/modules/login/models/authentication_state.model.dart b/mobile/lib/modules/login/models/authentication_state.model.dart index 648670ca6..9dcd320c8 100644 --- a/mobile/lib/modules/login/models/authentication_state.model.dart +++ b/mobile/lib/modules/login/models/authentication_state.model.dart @@ -3,8 +3,7 @@ class AuthenticationState { final String userId; final String userEmail; final bool isAuthenticated; - final String firstName; - final String lastName; + final String name; final bool isAdmin; final bool shouldChangePassword; final String profileImagePath; @@ -13,8 +12,7 @@ class AuthenticationState { required this.userId, required this.userEmail, required this.isAuthenticated, - required this.firstName, - required this.lastName, + required this.name, required this.isAdmin, required this.shouldChangePassword, required this.profileImagePath, @@ -25,8 +23,7 @@ class AuthenticationState { String? userId, String? userEmail, bool? isAuthenticated, - String? firstName, - String? lastName, + String? name, bool? isAdmin, bool? shouldChangePassword, String? profileImagePath, @@ -36,8 +33,7 @@ class AuthenticationState { userId: userId ?? this.userId, userEmail: userEmail ?? this.userEmail, isAuthenticated: isAuthenticated ?? this.isAuthenticated, - firstName: firstName ?? this.firstName, - lastName: lastName ?? this.lastName, + name: name ?? this.name, isAdmin: isAdmin ?? this.isAdmin, shouldChangePassword: shouldChangePassword ?? this.shouldChangePassword, profileImagePath: profileImagePath ?? this.profileImagePath, @@ -46,7 +42,7 @@ class AuthenticationState { @override String toString() { - return 'AuthenticationState(deviceId: $deviceId, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword, profileImagePath: $profileImagePath)'; + return 'AuthenticationState(deviceId: $deviceId, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, name: $name, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword, profileImagePath: $profileImagePath)'; } @override @@ -58,8 +54,7 @@ class AuthenticationState { other.userId == userId && other.userEmail == userEmail && other.isAuthenticated == isAuthenticated && - other.firstName == firstName && - other.lastName == lastName && + other.name == name && other.isAdmin == isAdmin && other.shouldChangePassword == shouldChangePassword && other.profileImagePath == profileImagePath; @@ -71,8 +66,7 @@ class AuthenticationState { userId.hashCode ^ userEmail.hashCode ^ isAuthenticated.hashCode ^ - firstName.hashCode ^ - lastName.hashCode ^ + name.hashCode ^ isAdmin.hashCode ^ shouldChangePassword.hashCode ^ profileImagePath.hashCode; diff --git a/mobile/lib/modules/login/providers/authentication.provider.dart b/mobile/lib/modules/login/providers/authentication.provider.dart index 33d2c0f3e..14a094963 100644 --- a/mobile/lib/modules/login/providers/authentication.provider.dart +++ b/mobile/lib/modules/login/providers/authentication.provider.dart @@ -26,8 +26,7 @@ class AuthenticationNotifier extends StateNotifier { deviceId: "", userId: "", userEmail: "", - firstName: '', - lastName: '', + name: '', profileImagePath: '', isAdmin: false, shouldChangePassword: false, @@ -117,8 +116,7 @@ class AuthenticationNotifier extends StateNotifier { deviceId: "", userId: "", userEmail: "", - firstName: '', - lastName: '', + name: '', profileImagePath: '', isAdmin: false, shouldChangePassword: false, @@ -187,12 +185,15 @@ class AuthenticationNotifier extends StateNotifier { if (userResponseDto != null) { Store.put(StoreKey.deviceId, deviceId); Store.put(StoreKey.deviceIdHash, fastHash(deviceId)); - Store.put(StoreKey.currentUser, User.fromDto(userResponseDto)); + Store.put( + StoreKey.currentUser, + User.fromUserDto(userResponseDto), + ); Store.put(StoreKey.serverUrl, serverUrl); Store.put(StoreKey.accessToken, accessToken); shouldChangePassword = userResponseDto.shouldChangePassword; - user = User.fromDto(userResponseDto); + user = User.fromUserDto(userResponseDto); retResult = true; } else { @@ -205,8 +206,7 @@ class AuthenticationNotifier extends StateNotifier { isAuthenticated: true, userId: user.id, userEmail: user.email, - firstName: user.firstName, - lastName: user.lastName, + name: user.name, profileImagePath: user.profileImagePath, isAdmin: user.isAdmin, shouldChangePassword: shouldChangePassword, diff --git a/mobile/lib/modules/login/ui/change_password_form.dart b/mobile/lib/modules/login/ui/change_password_form.dart index b7db83b3f..560967821 100644 --- a/mobile/lib/modules/login/ui/change_password_form.dart +++ b/mobile/lib/modules/login/ui/change_password_form.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; @@ -37,7 +38,7 @@ class ChangePasswordForm extends HookConsumerWidget { style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), Padding( @@ -45,8 +46,7 @@ class ChangePasswordForm extends HookConsumerWidget { child: Text( 'change_password_form_description'.tr( namedArgs: { - 'firstName': authState.firstName, - 'lastName': authState.lastName, + 'name': authState.name, }, ), style: TextStyle( @@ -191,7 +191,7 @@ class ChangePasswordButton extends ConsumerWidget { return ElevatedButton( style: ElevatedButton.styleFrom( visualDensity: VisualDensity.standard, - backgroundColor: Theme.of(context).primaryColor, + backgroundColor: context.primaryColor, foregroundColor: Colors.grey[50], elevation: 2, padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart index 58c7feec2..6c12742e4 100644 --- a/mobile/lib/modules/login/ui/login_form.dart +++ b/mobile/lib/modules/login/ui/login_form.dart @@ -1,9 +1,9 @@ import 'dart:io'; -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_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/modules/login/providers/oauth.provider.dart'; import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -48,7 +48,7 @@ class LoginForm extends HookConsumerWidget { /// Fetch the server login credential and enables oAuth login if necessary /// Returns true if successful, false otherwise Future getServerLoginCredential() async { - final serverUrl = serverEndpointController.text.trim(); + final serverUrl = sanitizeUrl(serverEndpointController.text); // Guard empty URL if (serverUrl.isEmpty) { @@ -127,6 +127,12 @@ class LoginForm extends HookConsumerWidget { ); populateTestLoginInfo() { + usernameController.text = 'demo@immich.app'; + passwordController.text = 'demo'; + serverEndpointController.text = 'https://demo.immich.app'; + } + + populateTestLoginInfo1() { usernameController.text = 'testuser@email.com'; passwordController.text = 'password'; serverEndpointController.text = 'http://10.1.15.216:2283/api'; @@ -144,13 +150,13 @@ class LoginForm extends HookConsumerWidget { await ref.read(authenticationProvider.notifier).login( usernameController.text, passwordController.text, - serverEndpointController.text.trim(), + sanitizeUrl(serverEndpointController.text), ); if (isAuthenticated) { // Resume backup (if enable) then navigate if (ref.read(authenticationProvider).shouldChangePassword && !ref.read(authenticationProvider).isAdmin) { - AutoRouter.of(context).push(const ChangePasswordRoute()); + context.autoPush(const ChangePasswordRoute()); } else { final hasPermission = await ref .read(galleryPermissionNotifier.notifier) @@ -159,7 +165,7 @@ class LoginForm extends HookConsumerWidget { // Don't resume the backup until we have gallery permission ref.read(backupProvider.notifier).resumeBackup(); } - AutoRouter.of(context).replace(const TabControllerRoute()); + context.autoReplace(const TabControllerRoute()); } } else { ImmichToast.show( @@ -181,7 +187,7 @@ class LoginForm extends HookConsumerWidget { try { oAuthServerConfig = await oAuthService - .getOAuthServerConfig(serverEndpointController.text); + .getOAuthServerConfig(sanitizeUrl(serverEndpointController.text)); isLoading.value = true; } catch (e) { @@ -203,7 +209,7 @@ class LoginForm extends HookConsumerWidget { .watch(authenticationProvider.notifier) .setSuccessLoginInfo( accessToken: loginResponseDto.accessToken, - serverUrl: serverEndpointController.text, + serverUrl: sanitizeUrl(serverEndpointController.text), ); if (isSuccess) { @@ -212,9 +218,7 @@ class LoginForm extends HookConsumerWidget { if (permission.isGranted || permission.isLimited) { ref.watch(backupProvider.notifier).resumeBackup(); } - AutoRouter.of(context).replace( - const TabControllerRoute(), - ); + context.autoReplace(const TabControllerRoute()); } else { ImmichToast.show( context: context, @@ -260,8 +264,7 @@ class LoginForm extends HookConsumerWidget { ), ), ), - onPressed: () => - AutoRouter.of(context).push(const SettingsRoute()), + onPressed: () => context.autoPush(const SettingsRoute()), icon: const Icon(Icons.settings_rounded), label: const SizedBox.shrink(), ), @@ -302,8 +305,8 @@ class LoginForm extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( - serverEndpointController.text, - style: Theme.of(context).textTheme.displaySmall, + sanitizeUrl(serverEndpointController.text), + style: context.textTheme.displaySmall, textAlign: TextAlign.center, ), if (isPasswordLoginEnable.value) ...[ @@ -339,8 +342,7 @@ class LoginForm extends HookConsumerWidget { horizontal: 16.0, ), child: Divider( - color: Brightness.dark == - Theme.of(context).brightness + color: context.isDarkTheme ? Colors.white : Colors.black, ), @@ -362,7 +364,7 @@ class LoginForm extends HookConsumerWidget { TextButton.icon( icon: const Icon(Icons.arrow_back), onPressed: () => serverEndpoint.value = null, - label: const Text('Back'), + label: const Text('login_form_back_button_text').tr(), ), ], ), @@ -391,6 +393,7 @@ class LoginForm extends HookConsumerWidget { children: [ GestureDetector( onDoubleTap: () => populateTestLoginInfo(), + onLongPress: () => populateTestLoginInfo1(), child: RotationTransition( turns: logoAnimationController, child: const ImmichLogo( @@ -588,7 +591,7 @@ class OAuthLoginButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor.withAlpha(230), + backgroundColor: context.primaryColor.withAlpha(230), padding: const EdgeInsets.symmetric(vertical: 12), ), onPressed: onPressed, diff --git a/mobile/lib/modules/login/views/login_page.dart b/mobile/lib/modules/login/views/login_page.dart index 98778736e..4e1b9a6df 100644 --- a/mobile/lib/modules/login/views/login_page.dart +++ b/mobile/lib/modules/login/views/login_page.dart @@ -1,7 +1,7 @@ -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/login/ui/login_form.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -47,13 +47,13 @@ class LoginPage extends HookConsumerWidget { child: Text( 'Logs', style: TextStyle( - color: Theme.of(context).primaryColor, + color: context.primaryColor, fontWeight: FontWeight.bold, fontFamily: "Inconsolata", ), ), onTap: () { - AutoRouter.of(context).push(const AppLogRoute()); + context.autoPush(const AppLogRoute()); }, ), ], diff --git a/mobile/lib/modules/map/models/map_state.model.dart b/mobile/lib/modules/map/models/map_state.model.dart index 7aab8efab..d606f1005 100644 --- a/mobile/lib/modules/map/models/map_state.model.dart +++ b/mobile/lib/modules/map/models/map_state.model.dart @@ -1,14 +1,20 @@ +import 'package:vector_map_tiles/vector_map_tiles.dart'; + class MapState { final bool isDarkTheme; final bool showFavoriteOnly; final bool includeArchived; final int relativeTime; + final Style? mapStyle; + final bool isLoading; MapState({ this.isDarkTheme = false, this.showFavoriteOnly = false, this.includeArchived = false, this.relativeTime = 0, + this.mapStyle, + this.isLoading = false, }); MapState copyWith({ @@ -16,18 +22,22 @@ class MapState { bool? showFavoriteOnly, bool? includeArchived, int? relativeTime, + Style? mapStyle, + bool? isLoading, }) { return MapState( isDarkTheme: isDarkTheme ?? this.isDarkTheme, showFavoriteOnly: showFavoriteOnly ?? this.showFavoriteOnly, includeArchived: includeArchived ?? this.includeArchived, relativeTime: relativeTime ?? this.relativeTime, + mapStyle: mapStyle ?? this.mapStyle, + isLoading: isLoading ?? this.isLoading, ); } @override String toString() { - return 'MapSettingsState(isDarkTheme: $isDarkTheme, showFavoriteOnly: $showFavoriteOnly, relativeTime: $relativeTime, includeArchived: $includeArchived)'; + return 'MapSettingsState(isDarkTheme: $isDarkTheme, showFavoriteOnly: $showFavoriteOnly, relativeTime: $relativeTime, includeArchived: $includeArchived, mapStyle: $mapStyle, isLoading: $isLoading)'; } @override @@ -38,7 +48,9 @@ class MapState { other.isDarkTheme == isDarkTheme && other.showFavoriteOnly == showFavoriteOnly && other.relativeTime == relativeTime && - other.includeArchived == includeArchived; + other.includeArchived == includeArchived && + other.mapStyle == mapStyle && + other.isLoading == isLoading; } @override @@ -46,6 +58,8 @@ class MapState { return isDarkTheme.hashCode ^ showFavoriteOnly.hashCode ^ relativeTime.hashCode ^ - includeArchived.hashCode; + includeArchived.hashCode ^ + mapStyle.hashCode ^ + isLoading.hashCode; } } diff --git a/mobile/lib/modules/map/providers/map_state.provider.dart b/mobile/lib/modules/map/providers/map_state.provider.dart index 8f4356198..3296b09c0 100644 --- a/mobile/lib/modules/map/providers/map_state.provider.dart +++ b/mobile/lib/modules/map/providers/map_state.provider.dart @@ -1,10 +1,23 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/map/models/map_state.model.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/shared/providers/api.provider.dart'; +import 'package:immich_mobile/shared/services/api.service.dart'; +import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; +import 'package:immich_mobile/utils/color_filter_generator.dart'; +import 'package:logging/logging.dart'; +import 'package:openapi/api.dart'; +import 'package:vector_map_tiles/vector_map_tiles.dart'; class MapStateNotifier extends StateNotifier { - MapStateNotifier(this._appSettingsProvider) + MapStateNotifier(this._appSettingsProvider, this._apiService) : super( MapState( isDarkTheme: _appSettingsProvider @@ -15,17 +28,69 @@ class MapStateNotifier extends StateNotifier { .getSetting(AppSettingsEnum.mapIncludeArchived), relativeTime: _appSettingsProvider .getSetting(AppSettingsEnum.mapRelativeDate), + isLoading: true, ), - ); + ) { + _fetchStyleFromServer( + _appSettingsProvider.getSetting(AppSettingsEnum.mapThemeMode), + ); + } final AppSettingsService _appSettingsProvider; + final ApiService _apiService; + final Logger _log = Logger("MapStateNotifier"); + + bool get isRaster => + state.mapStyle != null && state.mapStyle!.rasterTileProvider != null; + + double get maxZoom => + (isRaster ? state.mapStyle!.rasterTileProvider!.maximumZoom : 14) + .toDouble(); void switchTheme(bool isDarkTheme) { + _updateThemeMode(isDarkTheme); + _fetchStyleFromServer(isDarkTheme); + } + + void _updateThemeMode(bool isDarkTheme) { _appSettingsProvider.setSetting( AppSettingsEnum.mapThemeMode, isDarkTheme, ); - state = state.copyWith(isDarkTheme: isDarkTheme); + state = state.copyWith(isDarkTheme: isDarkTheme, isLoading: true); + } + + void _fetchStyleFromServer(bool isDarkTheme) async { + final styleResponse = await _apiService.systemConfigApi + .getMapStyleWithHttpInfo(isDarkTheme ? MapTheme.dark : MapTheme.light); + if (styleResponse.statusCode >= HttpStatus.badRequest) { + throw ApiException(styleResponse.statusCode, styleResponse.body); + } + final styleJsonString = styleResponse.body.isNotEmpty && + styleResponse.statusCode != HttpStatus.noContent + ? styleResponse.body + : null; + + if (styleJsonString == null) { + _log.severe('Style JSON from server is empty'); + return; + } + final styleJson = await compute(jsonDecode, styleJsonString); + if (styleJson is! Map) { + _log.severe('Style JSON from server is invalid'); + return; + } + final styleReader = StyleReader(uri: ''); + Style? style; + try { + style = await styleReader.readFromMap(styleJson); + } finally { + // Consume all error + } + state = state.copyWith( + mapStyle: style, + isLoading: false, + ); } void switchFavoriteOnly(bool isFavoriteOnly) { @@ -51,9 +116,44 @@ class MapStateNotifier extends StateNotifier { ); state = state.copyWith(relativeTime: relativeTime); } + + Widget getTileLayer([bool forceDark = false]) { + if (isRaster) { + final rasterProvider = state.mapStyle!.rasterTileProvider; + final rasterLayer = TileLayer( + urlTemplate: rasterProvider!.url, + maxNativeZoom: rasterProvider.maximumZoom, + maxZoom: rasterProvider.maximumZoom.toDouble(), + ); + return state.isDarkTheme || forceDark + ? InvertionFilter( + child: SaturationFilter( + saturation: -1, + child: BrightnessFilter( + brightness: -1, + child: rasterLayer, + ), + ), + ) + : rasterLayer; + } + if (state.mapStyle != null && !isRaster) { + return VectorTileLayer( + // Tiles and themes will be set for vector providers + tileProviders: state.mapStyle!.providers!, + theme: state.mapStyle!.theme!, + sprites: state.mapStyle!.sprites, + concurrency: 6, + ); + } + return const Center(child: ImmichLoadingIndicator()); + } } final mapStateNotifier = StateNotifierProvider((ref) { - return MapStateNotifier(ref.watch(appSettingsServiceProvider)); + return MapStateNotifier( + ref.watch(appSettingsServiceProvider), + ref.watch(apiServiceProvider), + ); }); diff --git a/mobile/lib/modules/map/ui/map_page_app_bar.dart b/mobile/lib/modules/map/ui/map_page_app_bar.dart index e9ed75cb1..ce426cf03 100644 --- a/mobile/lib/modules/map/ui/map_page_app_bar.dart +++ b/mobile/lib/modules/map/ui/map_page_app_bar.dart @@ -1,8 +1,8 @@ import 'dart:io'; -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/disable_multi_select_button.dart'; import 'package:immich_mobile/modules/map/ui/map_settings_dialog.dart'; @@ -30,7 +30,7 @@ class MapAppBar extends HookWidget implements PreferredSizeWidget { Padding( padding: const EdgeInsets.only(left: 15, top: 15), child: ElevatedButton( - onPressed: () => AutoRouter.of(context).pop(), + onPressed: () => context.autoPop(), style: ElevatedButton.styleFrom( shape: const CircleBorder(), padding: const EdgeInsets.all(12), diff --git a/mobile/lib/modules/map/ui/map_page_bottom_sheet.dart b/mobile/lib/modules/map/ui/map_page_bottom_sheet.dart index c9c3cb8aa..7e8ff13d0 100644 --- a/mobile/lib/modules/map/ui/map_page_bottom_sheet.dart +++ b/mobile/lib/modules/map/ui/map_page_bottom_sheet.dart @@ -5,6 +5,7 @@ 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/modules/asset_viewer/providers/render_list.provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; @@ -15,7 +16,6 @@ import 'package:immich_mobile/shared/ui/drag_sheet.dart'; import 'package:immich_mobile/utils/color_filter_generator.dart'; import 'package:immich_mobile/utils/debounce.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -import 'package:url_launcher/url_launcher.dart'; class MapPageBottomSheet extends StatefulHookConsumerWidget { final Stream mapPageEventStream; @@ -57,10 +57,10 @@ class AssetsInBoundBottomSheetState extends ConsumerState { @override Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; + final isDarkTheme = context.isDarkTheme; final bottomPadding = Platform.isAndroid ? MediaQuery.of(context).padding.bottom - 10 : 0.0; - final maxHeight = MediaQuery.of(context).size.height - bottomPadding; + final maxHeight = context.height - bottomPadding; final isSheetScrolled = useState(false); final isSheetExpanded = useState(false); final assetsInBound = useState([]); @@ -137,7 +137,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState { SizedBox( height: 150, width: 150, - child: isDarkMode + child: isDarkTheme ? const InvertionFilter( child: SaturationFilter( saturation: -1, @@ -156,7 +156,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState { "map_zoom_to_see_photos".tr(), style: TextStyle( fontSize: 20, - color: Theme.of(context).textTheme.displayLarge?.color, + color: context.textTheme.displayLarge?.color, ), ), ], @@ -176,13 +176,13 @@ class AssetsInBoundBottomSheetState extends ConsumerState { Widget buildDragHandle(ScrollController scrollController) { final textToDisplay = assetsInBound.value.isNotEmpty - ? "${assetsInBound.value.length} photo${assetsInBound.value.length > 1 ? "s" : ""}" + ? "map_assets_in_bounds".plural(assetsInBound.value.length) : "map_no_assets_in_bounds".tr(); final dragHandle = Container( - height: 60, + height: 70, width: double.infinity, decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.grey[100], + color: isDarkTheme ? Colors.grey[900] : Colors.grey[100], ), child: Stack( children: [ @@ -195,19 +195,12 @@ class AssetsInBoundBottomSheetState extends ConsumerState { const SizedBox(height: 15), Text( textToDisplay, - style: TextStyle( - fontSize: 16, - color: Theme.of(context).textTheme.displayLarge?.color, - fontWeight: FontWeight.bold, - ), + style: context.textTheme.bodyLarge, ), Divider( height: 10, - color: Theme.of(context) - .textTheme - .displayLarge - ?.color - ?.withOpacity(0.5), + color: + context.textTheme.displayLarge?.color?.withOpacity(0.5), ), ], ), @@ -218,7 +211,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState { child: IconButton( icon: Icon( Icons.map_outlined, - color: Theme.of(context).textTheme.displayLarge?.color, + color: context.textTheme.displayLarge?.color, ), iconSize: 20, tooltip: 'Zoom to bounds', @@ -266,7 +259,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState { ScrollController scrollController, ) { return Card( - color: isDarkMode ? Colors.grey[900] : Colors.grey[100], + color: isDarkTheme ? Colors.grey[900] : Colors.grey[100], surfaceTintColor: Colors.transparent, elevation: 18.0, margin: const EdgeInsets.all(0), @@ -320,24 +313,18 @@ class AssetsInBoundBottomSheetState extends ConsumerState { Positioned( bottom: maxHeight * currentExtend.value, left: 0, - child: GestureDetector( - onTap: () => launchUrl( - Uri.parse('https://openstreetmap.org/copyright'), - ), - child: ColoredBox( - color: (widget.isDarkTheme - ? Colors.grey[900] - : Colors.grey[100])!, - child: Padding( - padding: const EdgeInsets.all(3), - child: Text( - '© OpenStreetMap contributors', - style: TextStyle( - fontSize: 6, - color: !widget.isDarkTheme - ? Colors.grey[900] - : Colors.grey[100], - ), + child: ColoredBox( + color: + (widget.isDarkTheme ? Colors.grey[900] : Colors.grey[100])!, + child: Padding( + padding: const EdgeInsets.all(3), + child: Text( + 'OpenStreetMap contributors', + style: TextStyle( + fontSize: 6, + color: !widget.isDarkTheme + ? Colors.grey[900] + : Colors.grey[100], ), ), ), diff --git a/mobile/lib/modules/map/ui/map_settings_dialog.dart b/mobile/lib/modules/map/ui/map_settings_dialog.dart index f8a308b35..9619bd661 100644 --- a/mobile/lib/modules/map/ui/map_settings_dialog.dart +++ b/mobile/lib/modules/map/ui/map_settings_dialog.dart @@ -2,6 +2,7 @@ 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/modules/map/providers/map_state.provider.dart'; class MapSettingsDialog extends HookConsumerWidget { @@ -15,7 +16,7 @@ class MapSettingsDialog extends HookConsumerWidget { final showFavoriteOnly = useState(mapSettings.showFavoriteOnly); final showIncludeArchived = useState(mapSettings.includeArchived); final showRelativeDate = useState(mapSettings.relativeTime); - final ThemeData theme = Theme.of(context); + final ThemeData theme = context.themeData; Widget buildMapThemeSetting() { return SwitchListTile.adaptive( @@ -75,18 +76,21 @@ class MapSettingsDialog extends HookConsumerWidget { showRelativeDate.value = value!; }, dropdownMenuEntries: [ - const DropdownMenuEntry(value: 0, label: "All"), - const DropdownMenuEntry( + DropdownMenuEntry( + value: 0, + label: "map_settings_date_range_option_all".tr(), + ), + DropdownMenuEntry( value: 1, - label: "Past 24 hours", + label: "map_settings_date_range_option_days".plural(1), ), - const DropdownMenuEntry( + DropdownMenuEntry( value: 7, - label: "Past 7 days", + label: "map_settings_date_range_option_days".plural(7), ), - const DropdownMenuEntry( + DropdownMenuEntry( value: 30, - label: "Past 30 days", + label: "map_settings_date_range_option_days".plural(30), ), DropdownMenuEntry( value: now @@ -101,7 +105,7 @@ class MapSettingsDialog extends HookConsumerWidget { ), ) .inDays, - label: "Past year", + label: "map_settings_date_range_option_years".plural(1), ), DropdownMenuEntry( value: now @@ -116,7 +120,7 @@ class MapSettingsDialog extends HookConsumerWidget { ), ) .inDays, - label: "Past 3 years", + label: "map_settings_date_range_option_years".plural(3), ), ], ); @@ -125,17 +129,21 @@ class MapSettingsDialog extends HookConsumerWidget { List getDialogActions() { return [ TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), style: TextButton.styleFrom( backgroundColor: mapSettings.isDarkTheme ? Colors.grey[100] : Colors.grey[700], ), - child: Text( - "map_settings_dialog_cancel".tr(), - style: theme.textTheme.labelSmall?.copyWith( - fontWeight: FontWeight.bold, - color: - mapSettings.isDarkTheme ? Colors.grey[900] : Colors.grey[100], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + "map_settings_dialog_cancel".tr(), + style: theme.textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w500, + color: mapSettings.isDarkTheme + ? Colors.grey[900] + : Colors.grey[100], + ), ), ), ), @@ -146,16 +154,19 @@ class MapSettingsDialog extends HookConsumerWidget { mapSettingsNotifier.setRelativeTime(showRelativeDate.value); mapSettingsNotifier .switchIncludeArchived(showIncludeArchived.value); - Navigator.of(context).pop(); + context.pop(); }, style: TextButton.styleFrom( backgroundColor: theme.primaryColor, ), - child: Text( - "map_settings_dialog_save".tr(), - style: theme.textTheme.labelSmall?.copyWith( - fontWeight: FontWeight.bold, - color: theme.primaryTextTheme.labelLarge?.color, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + "map_settings_dialog_save".tr(), + style: theme.textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w500, + color: theme.primaryTextTheme.labelLarge?.color, + ), ), ), ), @@ -178,7 +189,7 @@ class MapSettingsDialog extends HookConsumerWidget { width: double.maxFinite, child: ConstrainedBox( constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.6, + maxHeight: context.height * 0.6, ), child: ListView( shrinkWrap: true, diff --git a/mobile/lib/modules/map/ui/map_thumbnail.dart b/mobile/lib/modules/map/ui/map_thumbnail.dart index 14cc2a83b..d42d99de1 100644 --- a/mobile/lib/modules/map/ui/map_thumbnail.dart +++ b/mobile/lib/modules/map/ui/map_thumbnail.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/shared/providers/server_info.provider.dart'; -import 'package:immich_mobile/utils/color_filter_generator.dart'; +import 'package:immich_mobile/modules/map/providers/map_state.provider.dart'; import 'package:latlong2/latlong.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -29,11 +28,7 @@ class MapThumbnail extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final tileLayer = TileLayer( - urlTemplate: ref.watch( - serverInfoProvider.select((v) => v.serverConfig.mapTileUrl), - ), - ); + ref.watch(mapStateNotifier.select((s) => s.mapStyle)); return SizedBox( height: height, @@ -55,20 +50,14 @@ class MapThumbnail extends HookConsumerWidget { 'OpenStreetMap contributors', onTap: () => launchUrl( Uri.parse('https://openstreetmap.org/copyright'), + mode: LaunchMode.externalApplication, ), ), ], ), ], children: [ - isDarkTheme - ? InvertionFilter( - child: SaturationFilter( - saturation: -1, - child: tileLayer, - ), - ) - : tileLayer, + ref.read(mapStateNotifier.notifier).getTileLayer(isDarkTheme), if (markers.isNotEmpty) MarkerLayer(markers: markers), ], ), diff --git a/mobile/lib/modules/map/views/map_page.dart b/mobile/lib/modules/map/views/map_page.dart index ffa18d37a..b03c13e36 100644 --- a/mobile/lib/modules/map/views/map_page.dart +++ b/mobile/lib/modules/map/views/map_page.dart @@ -1,6 +1,6 @@ import 'dart:async'; +import 'dart:math' as math; -import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -11,6 +11,7 @@ import 'package:flutter_map_heatmap/flutter_map_heatmap.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:geolocator/geolocator.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/map/models/map_page_event.model.dart'; import 'package:immich_mobile/modules/map/providers/map_marker.provider.dart'; import 'package:immich_mobile/modules/map/providers/map_state.provider.dart'; @@ -20,12 +21,10 @@ import 'package:immich_mobile/modules/map/ui/map_page_bottom_sheet.dart'; import 'package:immich_mobile/modules/map/ui/map_page_app_bar.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; -import 'package:immich_mobile/utils/color_filter_generator.dart'; import 'package:immich_mobile/utils/debounce.dart'; -import 'package:immich_mobile/utils/flutter_map_extensions.dart'; +import 'package:immich_mobile/extensions/flutter_map_extensions.dart'; import 'package:immich_mobile/utils/immich_app_theme.dart'; import 'package:immich_mobile/utils/selection_handlers.dart'; import 'package:latlong2/latlong.dart'; @@ -79,26 +78,30 @@ class MapPageState extends ConsumerState { Set? assetMarkers, { bool forceReload = false, }) { - final bounds = mapController.bounds; - if (bounds != null) { - final oldAssetsInBounds = assetsInBounds.toSet(); - assetsInBounds = - assetMarkers?.where((e) => bounds.contains(e.point)).toSet() ?? {}; - final shouldReload = forceReload || - assetsInBounds.difference(oldAssetsInBounds).isNotEmpty || - assetsInBounds.length != oldAssetsInBounds.length; - if (shouldReload) { - mapPageEventSC.add( - MapPageAssetsInBoundUpdated( - assetsInBounds.map((e) => e.asset).toList(), - ), - ); + try { + final bounds = mapController.bounds; + if (bounds != null) { + final oldAssetsInBounds = assetsInBounds.toSet(); + assetsInBounds = + assetMarkers?.where((e) => bounds.contains(e.point)).toSet() ?? {}; + final shouldReload = forceReload || + assetsInBounds.difference(oldAssetsInBounds).isNotEmpty || + assetsInBounds.length != oldAssetsInBounds.length; + if (shouldReload) { + mapPageEventSC.add( + MapPageAssetsInBoundUpdated( + assetsInBounds.map((e) => e.asset).toList(), + ), + ); + } } + } finally { + // Consume all error } } void openAssetInViewer(Asset asset) { - AutoRouter.of(context).push( + context.autoPush( GalleryViewerRoute( initialIndex: 0, loadAsset: (index) => asset, @@ -120,6 +123,10 @@ class MapPageState extends ConsumerState { final selectedAssets = useState({}); final showLoadingIndicator = useState(false); final refetchMarkers = useState(true); + final isLoading = + ref.watch(mapStateNotifier.select((state) => state.isLoading)); + final maxZoom = ref.read(mapStateNotifier.notifier).maxZoom; + final zoomLevel = math.min(maxZoom, 14.0); if (refetchMarkers.value) { mapMarkerData.value = ref.watch(mapMarkersProvider).when( @@ -168,7 +175,6 @@ class MapPageState extends ConsumerState { final mapMarker = mapMarkerData.value .firstWhereOrNull((e) => e.asset.id == assetInBottomSheet.id); if (mapMarker != null) { - const zoomLevel = 16.0; LatLng? newCenter = mapController.centerBoundsWithPadding( mapMarker.point, const Offset(0, -120), @@ -230,7 +236,7 @@ class MapPageState extends ConsumerState { forceAssetUpdate = true; mapController.move( LatLng(currentUserLocation.latitude, currentUserLocation.longitude), - 12, + zoomLevel, ); } catch (error) { log.severe( @@ -359,24 +365,6 @@ class MapPageState extends ConsumerState { selectedAssets.value = selection; } - final tileLayer = TileLayer( - urlTemplate: ref.watch( - serverInfoProvider.select((v) => v.serverConfig.mapTileUrl), - ), - maxNativeZoom: 19, - maxZoom: 19, - ); - - final darkTileLayer = InvertionFilter( - child: SaturationFilter( - saturation: -1, - child: BrightnessFilter( - brightness: -1, - child: tileLayer, - ), - ), - ); - final markerLayer = MarkerLayer( markers: [ if (closestAssetMarker.value != null) @@ -451,41 +439,43 @@ class MapPageState extends ConsumerState { extendBodyBehindAppBar: true, body: Stack( children: [ - FlutterMap( - mapController: mapController, - options: MapOptions( - maxBounds: - LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)), - interactiveFlags: InteractiveFlag.doubleTapZoom | - InteractiveFlag.drag | - InteractiveFlag.flingAnimation | - InteractiveFlag.pinchMove | - InteractiveFlag.pinchZoom, - center: LatLng(20, 20), - zoom: 2, - minZoom: 1, - maxZoom: 18, // max level supported by OSM, - onMapReady: () { - mapController.mapEventStream.listen(onMapEvent); - }, + if (!isLoading) + FlutterMap( + mapController: mapController, + options: MapOptions( + maxBounds: + LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)), + interactiveFlags: InteractiveFlag.doubleTapZoom | + InteractiveFlag.drag | + InteractiveFlag.flingAnimation | + InteractiveFlag.pinchMove | + InteractiveFlag.pinchZoom, + center: LatLng(20, 20), + zoom: 2, + minZoom: 1, + maxZoom: maxZoom, + onMapReady: () { + mapController.mapEventStream.listen(onMapEvent); + }, + ), + children: [ + ref.read(mapStateNotifier.notifier).getTileLayer(), + heatMapLayer, + markerLayer, + ], ), - children: [ - isDarkTheme ? darkTileLayer : tileLayer, - heatMapLayer, - markerLayer, - ], - ), - MapPageBottomSheet( - mapPageEventStream: mapPageEventSC.stream, - bottomSheetEventSC: bottomSheetEventSC, - selectionEnabled: selectionEnabledHook.value, - selectionlistener: selectionListener, - isDarkTheme: isDarkTheme, - ), - if (showLoadingIndicator.value) + if (!isLoading) + MapPageBottomSheet( + mapPageEventStream: mapPageEventSC.stream, + bottomSheetEventSC: bottomSheetEventSC, + selectionEnabled: selectionEnabledHook.value, + selectionlistener: selectionListener, + isDarkTheme: isDarkTheme, + ), + if (showLoadingIndicator.value || isLoading) Positioned( - top: MediaQuery.of(context).size.height * 0.35, - left: MediaQuery.of(context).size.width * 0.425, + top: context.height * 0.35, + left: context.width * 0.425, child: const ImmichLoadingIndicator(), ), ], diff --git a/mobile/lib/modules/memories/ui/memory_card.dart b/mobile/lib/modules/memories/ui/memory_card.dart index 8ef06e0d1..6c27ea748 100644 --- a/mobile/lib/modules/memories/ui/memory_card.dart +++ b/mobile/lib/modules/memories/ui/memory_card.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; @@ -34,10 +35,9 @@ class MemoryCard extends HookConsumerWidget { buildTitle() { return Text( title, - style: const TextStyle( + style: context.textTheme.headlineMedium?.copyWith( color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 24.0, + fontWeight: FontWeight.w500, ), ); } diff --git a/mobile/lib/modules/memories/ui/memory_lane.dart b/mobile/lib/modules/memories/ui/memory_lane.dart index dcd803651..0c709919b 100644 --- a/mobile/lib/modules/memories/ui/memory_lane.dart +++ b/mobile/lib/modules/memories/ui/memory_lane.dart @@ -1,7 +1,7 @@ -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/memories/providers/memory.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; @@ -17,7 +17,7 @@ class MemoryLane extends HookConsumerWidget { .whenData( (memories) => memories != null ? Container( - margin: const EdgeInsets.only(top: 10), + margin: const EdgeInsets.only(top: 10, left: 10), height: 200, child: ListView.builder( scrollDirection: Axis.horizontal, @@ -31,7 +31,7 @@ class MemoryLane extends HookConsumerWidget { child: GestureDetector( onTap: () { HapticFeedback.heavyImpact(); - AutoRouter.of(context).push( + context.autoPush( MemoryRoute( memories: memories, memoryIndex: index, @@ -48,7 +48,7 @@ class MemoryLane extends HookConsumerWidget { clipBehavior: Clip.hardEdge, child: ColorFiltered( colorFilter: ColorFilter.mode( - Colors.black.withOpacity(0.1), + Colors.black.withOpacity(0.2), BlendMode.darken, ), child: ImmichImage( @@ -71,9 +71,9 @@ class MemoryLane extends HookConsumerWidget { child: Text( memory.title, style: const TextStyle( - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, color: Colors.white, - fontSize: 14, + fontSize: 15, ), ), ), diff --git a/mobile/lib/modules/memories/views/memory_page.dart b/mobile/lib/modules/memories/views/memory_page.dart index a1aae5156..9c135961e 100644 --- a/mobile/lib/modules/memories/views/memory_page.dart +++ b/mobile/lib/modules/memories/views/memory_page.dart @@ -1,8 +1,8 @@ -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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/modules/memories/models/memory.dart'; import 'package:immich_mobile/modules/memories/ui/memory_card.dart'; import 'package:immich_mobile/shared/models/asset.dart'; @@ -138,8 +138,8 @@ class MemoryPage extends HookConsumerWidget { memory.title, style: TextStyle( color: Colors.grey[400], - fontSize: 11.0, - fontWeight: FontWeight.w600, + fontSize: 13.0, + fontWeight: FontWeight.w500, ), ), Text( @@ -148,7 +148,7 @@ class MemoryPage extends HookConsumerWidget { ), style: const TextStyle( color: Colors.white, - fontSize: 14.0, + fontSize: 15.0, fontWeight: FontWeight.w500, ), ), @@ -182,14 +182,14 @@ class MemoryPage extends HookConsumerWidget { currentMemory.value.assets.length; if (isLastAsset && (offset > notification.metrics.maxScrollExtent + 150)) { - AutoRouter.of(context).pop(); + context.autoPop(); return true; } } // Horizontal scroll handling if (notification.depth == 1 && (offset > notification.metrics.maxScrollExtent + 100)) { - AutoRouter.of(context).pop(); + context.autoPop(); return true; } } @@ -244,7 +244,7 @@ class MemoryPage extends HookConsumerWidget { child: MemoryCard( asset: asset, onTap: () => toNextAsset(index), - onClose: () => AutoRouter.of(context).pop(), + onClose: () => context.autoPop(), rightCornerText: assetProgress.value, title: memories[mIndex].title, showTitle: index == 0, diff --git a/mobile/lib/modules/onboarding/views/permission_onboarding_page.dart b/mobile/lib/modules/onboarding/views/permission_onboarding_page.dart index efbbc78a3..771deefa3 100644 --- a/mobile/lib/modules/onboarding/views/permission_onboarding_page.dart +++ b/mobile/lib/modules/onboarding/views/permission_onboarding_page.dart @@ -1,9 +1,7 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; -import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/ui/immich_logo.dart'; @@ -11,7 +9,6 @@ import 'package:immich_mobile/shared/ui/immich_title_text.dart'; import 'package:permission_handler/permission_handler.dart'; class PermissionOnboardingPage extends HookConsumerWidget { - const PermissionOnboardingPage({super.key}); @override @@ -19,16 +16,7 @@ class PermissionOnboardingPage extends HookConsumerWidget { final PermissionStatus permission = ref.watch(galleryPermissionNotifier); // Navigate to the main Tab Controller when permission is granted - void goToHome() { - // Resume backup (if enable) then navigate - ref.watch(backupProvider.notifier).resumeBackup() - .catchError((error) { - debugPrint('PermissionOnboardingPage error: $error'); - }); - AutoRouter.of(context).replace( - const TabControllerRoute(), - ); - } + void goToBackup() => context.autoReplace(const BackupControllerRoute()); // When the permission is denied, we show a request permission page buildRequestPermission() { @@ -38,21 +26,21 @@ class PermissionOnboardingPage extends HookConsumerWidget { children: [ Text( 'permission_onboarding_request', - style: Theme.of(context).textTheme.titleMedium, + style: context.textTheme.titleMedium, textAlign: TextAlign.center, ).tr(), const SizedBox(height: 18), ElevatedButton( onPressed: () => ref - .read(galleryPermissionNotifier.notifier) - .requestGalleryPermission() - .then((permission) async { - if (permission.isGranted) { - // If permission is limited, we will show the limited - // permission page - goToHome(); - } - }), + .read(galleryPermissionNotifier.notifier) + .requestGalleryPermission() + .then((permission) async { + if (permission.isGranted) { + // If permission is limited, we will show the limited + // permission page + goToBackup(); + } + }), child: const Text( 'permission_onboarding_grant_permission', ).tr(), @@ -70,12 +58,12 @@ class PermissionOnboardingPage extends HookConsumerWidget { children: [ Text( 'permission_onboarding_permission_granted', - style: Theme.of(context).textTheme.titleMedium, + style: context.textTheme.titleMedium, textAlign: TextAlign.center, ).tr(), const SizedBox(height: 18), ElevatedButton( - onPressed: () => goToHome(), + onPressed: () => goToBackup(), child: const Text('permission_onboarding_get_started').tr(), ), ], @@ -90,14 +78,15 @@ class PermissionOnboardingPage extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.warning_outlined, + const Icon( + Icons.warning_outlined, color: Colors.yellow, size: 48, ), const SizedBox(height: 8), Text( 'permission_onboarding_permission_limited', - style: Theme.of(context).textTheme.titleMedium, + style: context.textTheme.titleMedium, textAlign: TextAlign.center, ).tr(), const SizedBox(height: 18), @@ -109,7 +98,7 @@ class PermissionOnboardingPage extends HookConsumerWidget { ), const SizedBox(height: 8.0), TextButton( - onPressed: () => goToHome(), + onPressed: () => goToBackup(), child: const Text( 'permission_onboarding_continue_anyway', ).tr(), @@ -123,14 +112,15 @@ class PermissionOnboardingPage extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.warning_outlined, + const Icon( + Icons.warning_outlined, color: Colors.red, size: 48, ), const SizedBox(height: 8), Text( 'permission_onboarding_permission_denied', - style: Theme.of(context).textTheme.titleMedium, + style: context.textTheme.titleMedium, textAlign: TextAlign.center, ).tr(), const SizedBox(height: 18), @@ -183,16 +173,10 @@ class PermissionOnboardingPage extends HookConsumerWidget { ), ), TextButton( - child: const Text('permission_onboarding_log_out').tr(), - onPressed: () { - ref.read(authenticationProvider.notifier).logout(); - AutoRouter.of(context).replace( - const LoginRoute(), - ); - }, + child: const Text('permission_onboarding_back').tr(), + onPressed: () => context.autoPop(), ), ], - ), ), ), diff --git a/mobile/lib/modules/partner/providers/partner.provider.dart b/mobile/lib/modules/partner/providers/partner.provider.dart index d48435616..3123382bb 100644 --- a/mobile/lib/modules/partner/providers/partner.provider.dart +++ b/mobile/lib/modules/partner/providers/partner.provider.dart @@ -2,21 +2,31 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart'; +import 'package:immich_mobile/modules/partner/services/partner.service.dart'; import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:isar/isar.dart'; class PartnerSharedWithNotifier extends StateNotifier> { - PartnerSharedWithNotifier(Isar db) : super([]) { + PartnerSharedWithNotifier(Isar db, this._ps) : super([]) { final query = db.users.filter().isPartnerSharedWithEqualTo(true); query.findAll().then((partners) => state = partners); query.watch().listen((partners) => state = partners); } + + Future updatePartner(User partner, {required bool inTimeline}) { + return _ps.updatePartner(partner, inTimeline: inTimeline); + } + + final PartnerService _ps; } final partnerSharedWithProvider = StateNotifierProvider>((ref) { - return PartnerSharedWithNotifier(ref.watch(dbProvider)); + return PartnerSharedWithNotifier( + ref.watch(dbProvider), + ref.watch(partnerServiceProvider), + ); }); class PartnerSharedByNotifier extends StateNotifier> { diff --git a/mobile/lib/modules/partner/services/partner.service.dart b/mobile/lib/modules/partner/services/partner.service.dart index 42fcdb438..32e500353 100644 --- a/mobile/lib/modules/partner/services/partner.service.dart +++ b/mobile/lib/modules/partner/services/partner.service.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; +import 'package:openapi/api.dart'; final partnerServiceProvider = Provider( (ref) => PartnerService( @@ -36,7 +37,7 @@ class PartnerService { final userDtos = await _apiService.partnerApi.getPartners(direction._value); if (userDtos != null) { - return userDtos.map((u) => User.fromDto(u)).toList(); + return userDtos.map((u) => User.fromPartnerDto(u)).toList(); } } catch (e) { _log.warning("failed to get partners for direction $direction:\n$e"); @@ -69,4 +70,19 @@ class PartnerService { } return false; } + + Future updatePartner(User partner, {required bool inTimeline}) async { + try { + final dto = await _apiService.partnerApi + .updatePartner(partner.id, UpdatePartnerDto(inTimeline: inTimeline)); + if (dto != null) { + partner.inTimeline = dto.inTimeline ?? partner.inTimeline; + await _db.writeTxn(() => _db.users.put(partner)); + return true; + } + } catch (e) { + _log.warning("failed to update partner ${partner.id}:\n$e"); + } + return false; + } } diff --git a/mobile/lib/modules/partner/ui/partner_list.dart b/mobile/lib/modules/partner/ui/partner_list.dart index 64db045b1..6cf330509 100644 --- a/mobile/lib/modules/partner/ui/partner_list.dart +++ b/mobile/lib/modules/partner/ui/partner_list.dart @@ -1,6 +1,6 @@ -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/routing/router.dart'; import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/ui/user_avatar.dart'; @@ -21,17 +21,22 @@ class PartnerList extends HookConsumerWidget { Widget listEntry(BuildContext context, int index) { final User p = partner[index]; return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 12.0), - leading: userAvatar(context, p, radius: 30), + contentPadding: const EdgeInsets.only( + left: 12.0, + right: 18.0, + ), + leading: userAvatar(context, p, radius: 24), title: Text( - "${p.firstName} ${p.lastName}'s photos", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: Theme.of(context).primaryColor, + "${p.name}'s photos", + style: context.textTheme.labelLarge, + ), + trailing: Text( + "View all", + style: context.textTheme.labelLarge?.copyWith( + color: context.primaryColor, ), ), - onTap: () => AutoRouter.of(context).push(PartnerDetailRoute(partner: p)), + onTap: () => context.autoPush((PartnerDetailRoute(partner: p))), ); } } diff --git a/mobile/lib/modules/partner/views/partner_detail_page.dart b/mobile/lib/modules/partner/views/partner_detail_page.dart index 995eebd3f..28d53646d 100644 --- a/mobile/lib/modules/partner/views/partner_detail_page.dart +++ b/mobile/lib/modules/partner/views/partner_detail_page.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; +import 'package:immich_mobile/modules/partner/providers/partner.provider.dart'; import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; +import 'package:immich_mobile/shared/ui/immich_toast.dart'; class PartnerDetailPage extends HookConsumerWidget { const PartnerDetailPage({Key? key, required this.partner}) : super(key: key); @@ -14,6 +16,8 @@ class PartnerDetailPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final assets = ref.watch(assetsProvider(partner.isarId)); + final inTimeline = useState(partner.inTimeline); + bool toggleInProcess = false; useEffect( () { @@ -23,18 +27,56 @@ class PartnerDetailPage extends HookConsumerWidget { [], ); + void toggleInTimeline() async { + if (toggleInProcess) return; + toggleInProcess = true; + try { + final ok = await ref + .read(partnerSharedWithProvider.notifier) + .updatePartner(partner, inTimeline: !inTimeline.value); + if (ok) { + inTimeline.value = !inTimeline.value; + final action = inTimeline.value ? "shown on" : "hidden from"; + ImmichToast.show( + context: context, + toastType: ToastType.success, + durationInSecond: 1, + msg: "${partner.name}'s assets $action your timeline", + ); + } else { + ImmichToast.show( + context: context, + toastType: ToastType.error, + durationInSecond: 1, + msg: "Failed to toggle the timeline setting", + ); + } + } finally { + toggleInProcess = false; + } + } + return Scaffold( appBar: AppBar( - title: Text("${partner.firstName} ${partner.lastName}"), + title: Text(partner.name), elevation: 0, centerTitle: false, + actions: [ + IconButton( + onPressed: toggleInTimeline, + icon: Icon( + inTimeline.value ? Icons.collections : Icons.collections_outlined, + ), + tooltip: "Show/hide photos on your main timeline", + ), + ], ), - body: assets.when( - data: (renderList) => renderList.isEmpty + body: assets.widgetWhen( + onData: (renderList) => renderList.isEmpty ? Padding( padding: const EdgeInsets.all(16), child: Text( - "It seems ${partner.firstName} does not have any photos...\n" + "It seems ${partner.name} does not have any photos...\n" "Or your server version does not match the app version."), ) : ImmichAssetGrid( @@ -42,8 +84,6 @@ class PartnerDetailPage extends HookConsumerWidget { onRefresh: () => ref.read(assetProvider.notifier).getPartnerAssets(partner), ), - error: (e, _) => Text("Error loading partners:\n$e"), - loading: () => const Center(child: ImmichLoadingIndicator()), ), ); } diff --git a/mobile/lib/modules/partner/views/partner_page.dart b/mobile/lib/modules/partner/views/partner_page.dart index 61c639746..e90250df5 100644 --- a/mobile/lib/modules/partner/views/partner_page.dart +++ b/mobile/lib/modules/partner/views/partner_page.dart @@ -1,6 +1,7 @@ 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/modules/partner/providers/partner.provider.dart'; import 'package:immich_mobile/modules/partner/services/partner.service.dart'; import 'package:immich_mobile/shared/models/user.dart'; @@ -34,14 +35,14 @@ class PartnerPage extends HookConsumerWidget { children: [ for (User u in users) SimpleDialogOption( - onPressed: () => Navigator.pop(context, u), + onPressed: () => context.pop(u), child: Row( children: [ Padding( padding: const EdgeInsets.only(right: 8), child: userAvatar(context, u), ), - Text("${u.firstName} ${u.lastName}"), + Text(u.name), ], ), ), @@ -70,8 +71,7 @@ class PartnerPage extends HookConsumerWidget { builder: (BuildContext context) { return ConfirmDialog( title: "partner_page_stop_sharing_title", - content: - "partner_page_stop_sharing_content".tr(args: [u.firstName]), + content: "partner_page_stop_sharing_content".tr(args: [u.name]), onOk: () => ref.read(partnerServiceProvider).removePartner(u), ); }, @@ -118,6 +118,7 @@ class PartnerPage extends HookConsumerWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 8), @@ -126,12 +127,15 @@ class PartnerPage extends HookConsumerWidget { style: TextStyle(fontSize: 14), ).tr(), ), - ElevatedButton.icon( - onPressed: availableUsers.whenOrNull( - data: (data) => addNewUsersHandler, + Align( + alignment: Alignment.center, + child: ElevatedButton.icon( + onPressed: availableUsers.whenOrNull( + data: (data) => addNewUsersHandler, + ), + icon: const Icon(Icons.person_add), + label: const Text("partner_page_add_partner").tr(), ), - icon: const Icon(Icons.person_add), - label: const Text("partner_page_add_partner").tr(), ), ], ), diff --git a/mobile/lib/modules/search/providers/people.provider.dart b/mobile/lib/modules/search/providers/people.provider.dart index e40ff3fc8..6009ee53a 100644 --- a/mobile/lib/modules/search/providers/people.provider.dart +++ b/mobile/lib/modules/search/providers/people.provider.dart @@ -1,44 +1,51 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/search/models/curated_content.dart'; import 'package:immich_mobile/modules/search/services/person.service.dart'; -import 'package:openapi/api.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; -final personAssetsProvider = FutureProvider.family - .autoDispose((ref, personId) async { - final PersonService personService = ref.watch(personServiceProvider); +part 'people.provider.g.dart'; +@riverpod +Future> getCuratedPeople( + GetCuratedPeopleRef ref, +) async { + final PersonService personService = ref.read(personServiceProvider); + + final curatedPeople = await personService.getCuratedPeople(); + + return curatedPeople + .map((p) => CuratedContent(id: p.id, label: p.name)) + .toList(); +} + +@riverpod +Future personAssets(PersonAssetsRef ref, String personId) async { + final PersonService personService = ref.read(personServiceProvider); final assets = await personService.getPersonAssets(personId); - if (assets == null) { return RenderList.empty(); } - return RenderList.fromAssets(assets, GroupAssetsBy.auto); -}); - -final getCuratedPeopleProvider = - FutureProvider.autoDispose>((ref) async { - final PersonService personService = ref.watch(personServiceProvider); - - final curatedPeople = await personService.getCuratedPeople(); - - return curatedPeople ?? []; -}); - -class UpdatePersonName { - final String id; - final String name; - - UpdatePersonName(this.id, this.name); + final settings = ref.read(appSettingsServiceProvider); + final groupBy = + GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; + return await RenderList.fromAssets(assets, groupBy); } -final updatePersonNameProvider = - StateProvider.family((ref, dto) async { - final PersonService personService = ref.watch(personServiceProvider); +@riverpod +Future updatePersonName( + UpdatePersonNameRef ref, + String personId, + String updatedName, +) async { + final PersonService personService = ref.read(personServiceProvider); + final person = await personService.updateName(personId, updatedName); - final person = await personService.updateName(dto.id, dto.name); - - if (person != null && person.name == dto.name) { + if (person != null && person.name == updatedName) { ref.invalidate(getCuratedPeopleProvider); + return true; } -}); + return false; +} diff --git a/mobile/lib/modules/search/providers/people.provider.g.dart b/mobile/lib/modules/search/providers/people.provider.g.dart new file mode 100644 index 000000000..c13c2c160 --- /dev/null +++ b/mobile/lib/modules/search/providers/people.provider.g.dart @@ -0,0 +1,320 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'people.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$getCuratedPeopleHash() => r'2a534553812abe69abce2c2e41aa62b8de16e9d0'; + +/// See also [getCuratedPeople]. +@ProviderFor(getCuratedPeople) +final getCuratedPeopleProvider = + AutoDisposeFutureProvider>.internal( + getCuratedPeople, + name: r'getCuratedPeopleProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$getCuratedPeopleHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef GetCuratedPeopleRef + = AutoDisposeFutureProviderRef>; +String _$personAssetsHash() => r'1d6eff5ca3aa630b58c4dad9516193b21896984d'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [personAssets]. +@ProviderFor(personAssets) +const personAssetsProvider = PersonAssetsFamily(); + +/// See also [personAssets]. +class PersonAssetsFamily extends Family> { + /// See also [personAssets]. + const PersonAssetsFamily(); + + /// See also [personAssets]. + PersonAssetsProvider call( + String personId, + ) { + return PersonAssetsProvider( + personId, + ); + } + + @override + PersonAssetsProvider getProviderOverride( + covariant PersonAssetsProvider provider, + ) { + return call( + provider.personId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'personAssetsProvider'; +} + +/// See also [personAssets]. +class PersonAssetsProvider extends AutoDisposeFutureProvider { + /// See also [personAssets]. + PersonAssetsProvider( + String personId, + ) : this._internal( + (ref) => personAssets( + ref as PersonAssetsRef, + personId, + ), + from: personAssetsProvider, + name: r'personAssetsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$personAssetsHash, + dependencies: PersonAssetsFamily._dependencies, + allTransitiveDependencies: + PersonAssetsFamily._allTransitiveDependencies, + personId: personId, + ); + + PersonAssetsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.personId, + }) : super.internal(); + + final String personId; + + @override + Override overrideWith( + FutureOr Function(PersonAssetsRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: PersonAssetsProvider._internal( + (ref) => create(ref as PersonAssetsRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + personId: personId, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _PersonAssetsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is PersonAssetsProvider && other.personId == personId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, personId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin PersonAssetsRef on AutoDisposeFutureProviderRef { + /// The parameter `personId` of this provider. + String get personId; +} + +class _PersonAssetsProviderElement + extends AutoDisposeFutureProviderElement with PersonAssetsRef { + _PersonAssetsProviderElement(super.provider); + + @override + String get personId => (origin as PersonAssetsProvider).personId; +} + +String _$updatePersonNameHash() => r'c7179a7cc558669c3b30b03fbca7782a42f2b6fd'; + +/// See also [updatePersonName]. +@ProviderFor(updatePersonName) +const updatePersonNameProvider = UpdatePersonNameFamily(); + +/// See also [updatePersonName]. +class UpdatePersonNameFamily extends Family> { + /// See also [updatePersonName]. + const UpdatePersonNameFamily(); + + /// See also [updatePersonName]. + UpdatePersonNameProvider call( + String personId, + String updatedName, + ) { + return UpdatePersonNameProvider( + personId, + updatedName, + ); + } + + @override + UpdatePersonNameProvider getProviderOverride( + covariant UpdatePersonNameProvider provider, + ) { + return call( + provider.personId, + provider.updatedName, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'updatePersonNameProvider'; +} + +/// See also [updatePersonName]. +class UpdatePersonNameProvider extends AutoDisposeFutureProvider { + /// See also [updatePersonName]. + UpdatePersonNameProvider( + String personId, + String updatedName, + ) : this._internal( + (ref) => updatePersonName( + ref as UpdatePersonNameRef, + personId, + updatedName, + ), + from: updatePersonNameProvider, + name: r'updatePersonNameProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$updatePersonNameHash, + dependencies: UpdatePersonNameFamily._dependencies, + allTransitiveDependencies: + UpdatePersonNameFamily._allTransitiveDependencies, + personId: personId, + updatedName: updatedName, + ); + + UpdatePersonNameProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.personId, + required this.updatedName, + }) : super.internal(); + + final String personId; + final String updatedName; + + @override + Override overrideWith( + FutureOr Function(UpdatePersonNameRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: UpdatePersonNameProvider._internal( + (ref) => create(ref as UpdatePersonNameRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + personId: personId, + updatedName: updatedName, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _UpdatePersonNameProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is UpdatePersonNameProvider && + other.personId == personId && + other.updatedName == updatedName; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, personId.hashCode); + hash = _SystemHash.combine(hash, updatedName.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin UpdatePersonNameRef on AutoDisposeFutureProviderRef { + /// The parameter `personId` of this provider. + String get personId; + + /// The parameter `updatedName` of this provider. + String get updatedName; +} + +class _UpdatePersonNameProviderElement + extends AutoDisposeFutureProviderElement with UpdatePersonNameRef { + _UpdatePersonNameProviderElement(super.provider); + + @override + String get personId => (origin as UpdatePersonNameProvider).personId; + @override + String get updatedName => (origin as UpdatePersonNameProvider).updatedName; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/modules/search/services/person.service.dart b/mobile/lib/modules/search/services/person.service.dart index 8314ed109..d4cbe0de5 100644 --- a/mobile/lib/modules/search/services/person.service.dart +++ b/mobile/lib/modules/search/services/person.service.dart @@ -1,44 +1,40 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; +import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; -final personServiceProvider = Provider( - (ref) => PersonService( - ref.watch(apiServiceProvider), - ), -); +part 'person.service.g.dart'; + +@riverpod +PersonService personService(PersonServiceRef ref) => + PersonService(ref.read(apiServiceProvider)); class PersonService { + final Logger _log = Logger("PersonService"); final ApiService _apiService; PersonService(this._apiService); - Future?> getCuratedPeople() async { + Future> getCuratedPeople() async { try { final peopleResponseDto = await _apiService.personApi.getAllPeople(); - return peopleResponseDto?.people; - } catch (e) { - debugPrint("Error [getCuratedPeople] ${e.toString()}"); - return null; + return peopleResponseDto?.people ?? []; + } catch (error, stack) { + _log.severe("Error while fetching curated people", error, stack); + return []; } } Future?> getPersonAssets(String id) async { try { final assets = await _apiService.personApi.getPersonAssets(id); - - if (assets == null) { - return null; - } - - return assets.map((e) => Asset.remote(e)).toList(); - } catch (e) { - debugPrint("Error [getPersonAssets] ${e.toString()}"); - return null; + return assets?.map((e) => Asset.remote(e)).toList(); + } catch (error, stack) { + _log.severe("Error while fetching person assets", error, stack); } + return null; } Future updateName(String id, String name) async { @@ -49,9 +45,9 @@ class PersonService { name: name, ), ); - } catch (e) { - debugPrint("Error [updateName] ${e.toString()}"); - return null; + } catch (error, stack) { + _log.severe("Error while updating person name", error, stack); } + return null; } } diff --git a/mobile/lib/modules/search/services/person.service.g.dart b/mobile/lib/modules/search/services/person.service.g.dart new file mode 100644 index 000000000..e66c6c2aa --- /dev/null +++ b/mobile/lib/modules/search/services/person.service.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'person.service.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$personServiceHash() => r'3fc3dcf4603c7b55c0deae65f39f6c212eea492b'; + +/// See also [personService]. +@ProviderFor(personService) +final personServiceProvider = AutoDisposeProvider.internal( + personService, + name: r'personServiceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$personServiceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef PersonServiceRef = AutoDisposeProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/modules/search/ui/curated_people_row.dart b/mobile/lib/modules/search/ui/curated_people_row.dart index 8a65c25f7..e838c59e1 100644 --- a/mobile/lib/modules/search/ui/curated_people_row.dart +++ b/mobile/lib/modules/search/ui/curated_people_row.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/search/models/curated_content.dart'; import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; import 'package:immich_mobile/shared/models/store.dart'; @@ -83,9 +84,8 @@ class CuratedPeopleRow extends StatelessWidget { padding: const EdgeInsets.only(top: 8.0), child: Text( "Add name", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, + style: context.textTheme.labelLarge?.copyWith( + color: context.primaryColor, ), ), ), @@ -97,10 +97,7 @@ class CuratedPeopleRow extends StatelessWidget { person.label, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 13.0, - ), + style: context.textTheme.labelLarge, ), ), ], diff --git a/mobile/lib/modules/search/ui/curated_places_row.dart b/mobile/lib/modules/search/ui/curated_places_row.dart index d62607a80..b0343f5ed 100644 --- a/mobile/lib/modules/search/ui/curated_places_row.dart +++ b/mobile/lib/modules/search/ui/curated_places_row.dart @@ -1,5 +1,6 @@ -import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart'; import 'package:immich_mobile/modules/search/ui/curated_row.dart'; import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; @@ -25,7 +26,7 @@ class CuratedPlacesRow extends CuratedRow { final int actualContentIndex = isMapEnabled ? 1 : 0; Widget buildMapThumbnail() { return GestureDetector( - onTap: () => AutoRouter.of(context).push( + onTap: () => context.autoPush( const MapRoute(), ), child: SizedBox( @@ -43,36 +44,39 @@ class CuratedPlacesRow extends CuratedRow { ), height: imageSize, showAttribution: false, - isDarkTheme: Theme.of(context).brightness == Brightness.dark, + isDarkTheme: context.isDarkTheme, ), ), - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.black, - gradient: LinearGradient( - begin: FractionalOffset.topCenter, - end: FractionalOffset.bottomCenter, - colors: [ - Colors.blueGrey.withOpacity(0.0), - Colors.black.withOpacity(0.4), - ], - stops: const [0.0, 1.0], + Padding( + padding: const EdgeInsets.only(right: 10.0), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.black, + gradient: LinearGradient( + begin: FractionalOffset.topCenter, + end: FractionalOffset.bottomCenter, + colors: [ + Colors.blueGrey.withOpacity(0.0), + Colors.black.withOpacity(0.4), + ], + stops: const [0.0, 0.4], + ), ), ), ), - const Align( + Align( alignment: Alignment.bottomCenter, child: Padding( - padding: EdgeInsets.only(bottom: 10), - child: Text( - "Your Map", + padding: const EdgeInsets.only(bottom: 10), + child: const Text( + "search_page_your_map", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), - ), + ).tr(), ), ), ], diff --git a/mobile/lib/modules/search/ui/explore_grid.dart b/mobile/lib/modules/search/ui/explore_grid.dart index 12f8ec6a1..984f65a40 100644 --- a/mobile/lib/modules/search/ui/explore_grid.dart +++ b/mobile/lib/modules/search/ui/explore_grid.dart @@ -1,5 +1,5 @@ -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/modules/search/models/curated_content.dart'; import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -50,13 +50,13 @@ class ExploreGrid extends StatelessWidget { borderRadius: 0, onTap: () { isPeople - ? AutoRouter.of(context).push( + ? context.autoPush( PersonResultRoute( personId: content.id, personName: content.label, ), ) - : AutoRouter.of(context).push( + : context.autoPush( SearchResultRoute(searchTerm: 'm:${content.label}'), ); }, diff --git a/mobile/lib/modules/search/ui/immich_search_bar.dart b/mobile/lib/modules/search/ui/immich_search_bar.dart index 5ab4bc964..b3275237f 100644 --- a/mobile/lib/modules/search/ui/immich_search_bar.dart +++ b/mobile/lib/modules/search/ui/immich_search_bar.dart @@ -2,6 +2,7 @@ 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/modules/search/providers/search_page_state.provider.dart'; class ImmichSearchBar extends HookConsumerWidget @@ -57,11 +58,9 @@ class ImmichSearchBar extends HookConsumerWidget }, decoration: InputDecoration( hintText: 'search_bar_hint'.tr(), - hintStyle: Theme.of(context).textTheme.titleSmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5), - fontWeight: FontWeight.w500, - fontSize: 14, - ), + hintStyle: context.textTheme.bodyLarge?.copyWith( + color: context.themeData.colorScheme.onSurface.withOpacity(0.75), + ), enabledBorder: const UnderlineInputBorder( borderSide: BorderSide(color: Colors.transparent), ), diff --git a/mobile/lib/modules/search/ui/person_name_edit_form.dart b/mobile/lib/modules/search/ui/person_name_edit_form.dart index f60824b5f..e32d4a9e0 100644 --- a/mobile/lib/modules/search/ui/person_name_edit_form.dart +++ b/mobile/lib/modules/search/ui/person_name_edit_form.dart @@ -1,6 +1,8 @@ +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/modules/search/providers/people.provider.dart'; class PersonNameEditFormResult { @@ -23,58 +25,55 @@ class PersonNameEditForm extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final controller = useTextEditingController(text: personName); + final isError = useState(false); return AlertDialog( title: const Text( - "Add a name", + "search_page_person_add_name_dialog_title", style: TextStyle(fontWeight: FontWeight.bold), - ), + ).tr(), content: SingleChildScrollView( child: TextFormField( controller: controller, autofocus: true, - decoration: const InputDecoration( - hintText: 'Name', + decoration: InputDecoration( + hintText: 'search_page_person_add_name_dialog_hint'.tr(), + border: const OutlineInputBorder(), + errorText: isError.value ? 'Error occured' : null, ), ), ), actions: [ TextButton( - style: TextButton.styleFrom(), - onPressed: () { - Navigator.of(context, rootNavigator: true) - .pop( - PersonNameEditFormResult(false, ''), - ); - }, + onPressed: () => context.pop( + PersonNameEditFormResult(false, ''), + ), child: Text( - "Cancel", + "search_page_person_add_name_dialog_cancel", style: TextStyle( color: Colors.red[300], fontWeight: FontWeight.bold, ), - ), + ).tr(), ), TextButton( - onPressed: () { - ref.read( - updatePersonNameProvider( - UpdatePersonName(personId, controller.text), - ), - ); - - Navigator.of(context, rootNavigator: true) - .pop( - PersonNameEditFormResult(true, controller.text), + onPressed: () async { + isError.value = false; + final result = await ref.read( + updatePersonNameProvider(personId, controller.text).future, ); + isError.value = !result; + if (result) { + context.pop(PersonNameEditFormResult(true, controller.text)); + } }, child: Text( - "Save", + "search_page_person_add_name_dialog_save", style: TextStyle( - color: Theme.of(context).primaryColor, + color: context.primaryColor, fontWeight: FontWeight.bold, ), - ), + ).tr(), ), ], ); diff --git a/mobile/lib/modules/search/ui/search_row_title.dart b/mobile/lib/modules/search/ui/search_row_title.dart index 5448874e3..830bc94c9 100644 --- a/mobile/lib/modules/search/ui/search_row_title.dart +++ b/mobile/lib/modules/search/ui/search_row_title.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; class SearchRowTitle extends StatelessWidget { final Function() onViewAllPressed; @@ -26,16 +27,16 @@ class SearchRowTitle extends StatelessWidget { children: [ Text( title, - style: Theme.of(context).textTheme.titleSmall, + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), ), TextButton( onPressed: onViewAllPressed, child: Text( 'search_page_view_all_button', - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - fontSize: 14.0, + style: context.textTheme.labelLarge?.copyWith( + color: context.primaryColor, ), ).tr(), ), diff --git a/mobile/lib/modules/search/ui/search_suggestion_list.dart b/mobile/lib/modules/search/ui/search_suggestion_list.dart index b66be410f..b5f5029c3 100644 --- a/mobile/lib/modules/search/ui/search_suggestion_list.dart +++ b/mobile/lib/modules/search/ui/search_suggestion_list.dart @@ -1,6 +1,7 @@ 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/modules/search/providers/search_page_state.provider.dart'; class SearchSuggestionList extends ConsumerWidget { @@ -13,17 +14,16 @@ class SearchSuggestionList extends ConsumerWidget { final searchTerm = ref.watch(searchPageStateProvider).searchTerm; final searchSuggestion = ref.watch(searchPageStateProvider).searchSuggestion; - var isDarkTheme = Theme.of(context).brightness == Brightness.dark; return Container( color: searchTerm.isEmpty ? Colors.black.withOpacity(0.5) - : Theme.of(context).scaffoldBackgroundColor, + : context.scaffoldBackgroundColor, child: CustomScrollView( slivers: [ SliverToBoxAdapter( child: Container( - color: isDarkTheme ? Colors.grey[800] : Colors.grey[100], + color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[100], child: Padding( padding: const EdgeInsets.all(16.0), child: RichText( @@ -31,14 +31,14 @@ class SearchSuggestionList extends ConsumerWidget { children: [ TextSpan( text: 'search_suggestion_list_smart_search_hint_1'.tr(), - style: Theme.of(context).textTheme.bodyMedium, + style: context.textTheme.bodyMedium, ), TextSpan( text: 'search_suggestion_list_smart_search_hint_2'.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - ), + style: context.textTheme.bodyMedium?.copyWith( + color: context.primaryColor, + fontWeight: FontWeight.bold, + ), ), ], ), diff --git a/mobile/lib/modules/search/ui/thumbnail_with_info.dart b/mobile/lib/modules/search/ui/thumbnail_with_info.dart index bbb7e6834..16714d830 100644 --- a/mobile/lib/modules/search/ui/thumbnail_with_info.dart +++ b/mobile/lib/modules/search/ui/thumbnail_with_info.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/models/store.dart'; -import 'package:immich_mobile/utils/capitalize.dart'; +import 'package:immich_mobile/extensions/string_extensions.dart'; // ignore: must_be_immutable class ThumbnailWithInfo extends StatelessWidget { @@ -22,8 +23,8 @@ class ThumbnailWithInfo extends StatelessWidget { @override Widget build(BuildContext context) { - var isDarkMode = Theme.of(context).brightness == Brightness.dark; - var textAndIconColor = isDarkMode ? Colors.grey[100] : Colors.grey[700]; + var textAndIconColor = + context.isDarkTheme ? Colors.grey[100] : Colors.grey[700]; return GestureDetector( onTap: () { onTap(); @@ -34,7 +35,7 @@ class ThumbnailWithInfo extends StatelessWidget { Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(borderRadius), - color: isDarkMode ? Colors.grey[900] : Colors.grey[100], + color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[100], ), child: imageUrl != null ? ClipRRect( diff --git a/mobile/lib/modules/search/views/all_motion_videos_page.dart b/mobile/lib/modules/search/views/all_motion_videos_page.dart index ba990a7da..8290f0dd6 100644 --- a/mobile/lib/modules/search/views/all_motion_videos_page.dart +++ b/mobile/lib/modules/search/views/all_motion_videos_page.dart @@ -1,10 +1,10 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/all_motion_photos.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; class AllMotionPhotosPage extends HookConsumerWidget { const AllMotionPhotosPage({super.key}); @@ -17,18 +17,14 @@ class AllMotionPhotosPage extends HookConsumerWidget { appBar: AppBar( title: const Text('motion_photos_page_title').tr(), leading: IconButton( - onPressed: () => AutoRouter.of(context).pop(), + onPressed: () => context.autoPop(), icon: const Icon(Icons.arrow_back_ios_rounded), ), ), - body: motionPhotos.when( - data: (assets) => ImmichAssetGrid( + body: motionPhotos.widgetWhen( + onData: (assets) => ImmichAssetGrid( assets: assets, ), - error: (e, s) => Text(e.toString()), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), ), ); } diff --git a/mobile/lib/modules/search/views/all_people_page.dart b/mobile/lib/modules/search/views/all_people_page.dart index d3361fc30..7a8183148 100644 --- a/mobile/lib/modules/search/views/all_people_page.dart +++ b/mobile/lib/modules/search/views/all_people_page.dart @@ -1,11 +1,10 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/search/models/curated_content.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/search/providers/people.provider.dart'; import 'package:immich_mobile/modules/search/ui/explore_grid.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; class AllPeoplePage extends HookConsumerWidget { const AllPeoplePage({super.key}); @@ -16,34 +15,18 @@ class AllPeoplePage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: Text( + title: const Text( 'all_people_page_title', - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - fontSize: 16.0, - ), ).tr(), leading: IconButton( - onPressed: () => AutoRouter.of(context).pop(), + onPressed: () => context.autoPop(), icon: const Icon(Icons.arrow_back_ios_rounded), ), ), - body: curatedPeople.when( - loading: () => const Center(child: ImmichLoadingIndicator()), - error: (err, stack) => Center( - child: Text('Error: $err'), - ), - data: (people) => ExploreGrid( + body: curatedPeople.widgetWhen( + onData: (people) => ExploreGrid( isPeople: true, - curatedContent: people - .map( - (person) => CuratedContent( - label: person.name, - id: person.id, - ), - ) - .toList(), + curatedContent: people, ), ), ); diff --git a/mobile/lib/modules/search/views/all_videos_page.dart b/mobile/lib/modules/search/views/all_videos_page.dart index 9461869ae..683539880 100644 --- a/mobile/lib/modules/search/views/all_videos_page.dart +++ b/mobile/lib/modules/search/views/all_videos_page.dart @@ -1,10 +1,10 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/all_video_assets.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; class AllVideosPage extends HookConsumerWidget { const AllVideosPage({super.key}); @@ -17,18 +17,14 @@ class AllVideosPage extends HookConsumerWidget { appBar: AppBar( title: const Text('all_videos_page_title').tr(), leading: IconButton( - onPressed: () => AutoRouter.of(context).pop(), + onPressed: () => context.autoPop(), icon: const Icon(Icons.arrow_back_ios_rounded), ), ), - body: videos.when( - data: (assets) => ImmichAssetGrid( + body: videos.widgetWhen( + onData: (assets) => ImmichAssetGrid( assets: assets, ), - error: (e, s) => Text(e.toString()), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), ), ); } diff --git a/mobile/lib/modules/search/views/curated_location_page.dart b/mobile/lib/modules/search/views/curated_location_page.dart index 59e4c3a87..6675e0826 100644 --- a/mobile/lib/modules/search/views/curated_location_page.dart +++ b/mobile/lib/modules/search/views/curated_location_page.dart @@ -1,11 +1,11 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/search/models/curated_content.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; import 'package:immich_mobile/modules/search/ui/explore_grid.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:openapi/api.dart'; class CuratedLocationPage extends HookConsumerWidget { @@ -18,25 +18,16 @@ class CuratedLocationPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: Text( + title: const Text( 'curated_location_page_title', - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - fontSize: 16.0, - ), ).tr(), leading: IconButton( - onPressed: () => AutoRouter.of(context).pop(), + onPressed: () => context.autoPop(), icon: const Icon(Icons.arrow_back_ios_rounded), ), ), - body: curatedLocation.when( - loading: () => const Center(child: ImmichLoadingIndicator()), - error: (err, stack) => Center( - child: Text('Error: $err'), - ), - data: (curatedLocations) => ExploreGrid( + body: curatedLocation.widgetWhen( + onData: (curatedLocations) => ExploreGrid( curatedContent: curatedLocations .map( (l) => CuratedContent( diff --git a/mobile/lib/modules/search/views/person_result_page.dart b/mobile/lib/modules/search/views/person_result_page.dart index 01483f0bd..40a2d1b14 100644 --- a/mobile/lib/modules/search/views/person_result_page.dart +++ b/mobile/lib/modules/search/views/person_result_page.dart @@ -1,7 +1,9 @@ -import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/people.provider.dart'; import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart'; @@ -23,12 +25,12 @@ class PersonResultPage extends HookConsumerWidget { final name = useState(personName); showEditNameDialog() { - showDialog( + showDialog( context: context, builder: (BuildContext context) { return PersonNameEditForm( personId: personId, - personName: personName, + personName: name.value, ); }, ).then((result) { @@ -40,7 +42,7 @@ class PersonResultPage extends HookConsumerWidget { void buildBottomSheet() { showModalBottomSheet( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, + backgroundColor: context.scaffoldBackgroundColor, isScrollControlled: false, context: context, useSafeArea: true, @@ -52,9 +54,9 @@ class PersonResultPage extends HookConsumerWidget { ListTile( leading: const Icon(Icons.edit_outlined), title: const Text( - 'Edit name', + 'search_page_person_edit_name', style: TextStyle(fontWeight: FontWeight.bold), - ), + ).tr(), onTap: showEditNameDialog, ), ], @@ -65,35 +67,33 @@ class PersonResultPage extends HookConsumerWidget { } buildTitleBlock() { - if (name.value == "") { - return GestureDetector( - onTap: showEditNameDialog, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Add a name', - style: Theme.of(context).textTheme.titleSmall?.copyWith( - color: Theme.of(context).colorScheme.secondary, + return GestureDetector( + onTap: showEditNameDialog, + child: name.value.isEmpty + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'search_page_person_add_name_title', + style: context.textTheme.titleMedium?.copyWith( + color: context.primaryColor, ), + ).tr(), + Text( + 'search_page_person_add_name_subtitle', + style: context.textTheme.labelLarge, + ).tr(), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name.value, + style: context.textTheme.titleLarge, + ), + ], ), - Text( - 'Find them fast by name with search', - style: Theme.of(context).textTheme.labelSmall, - ), - ], - ), - ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name.value, - style: Theme.of(context).textTheme.titleLarge, - ), - ], ); } @@ -101,7 +101,7 @@ class PersonResultPage extends HookConsumerWidget { appBar: AppBar( title: Text(name.value), leading: IconButton( - onPressed: () => AutoRouter.of(context).pop(), + onPressed: () => context.autoPop(), icon: const Icon(Icons.arrow_back_ios_rounded), ), actions: [ @@ -111,41 +111,31 @@ class PersonResultPage extends HookConsumerWidget { ), ], ), - body: ref.watch(personAssetsProvider(personId)).when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stackTrace) => Center( - child: Text( - error.toString(), - ), - ), - data: (data) => data.isEmpty - ? const Center( - child: Text('Opps'), - ) - : ImmichAssetGrid( - renderList: data, - topWidget: Padding( - padding: const EdgeInsets.only(left: 8.0, top: 24), - child: Row( - children: [ - CircleAvatar( - radius: 36, - backgroundImage: NetworkImage( - getFaceThumbnailUrl(personId), - headers: { - "Authorization": - "Bearer ${isar_store.Store.get(isar_store.StoreKey.accessToken)}", - }, - ), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: buildTitleBlock(), - ), - ], + body: ref.watch(personAssetsProvider(personId)).widgetWhen( + onData: (renderList) => ImmichAssetGrid( + renderList: renderList, + topWidget: Padding( + padding: const EdgeInsets.only(left: 8.0, top: 24), + child: Row( + children: [ + CircleAvatar( + radius: 36, + backgroundImage: NetworkImage( + getFaceThumbnailUrl(personId), + headers: { + "Authorization": + "Bearer ${isar_store.Store.get(isar_store.StoreKey.accessToken)}", + }, ), ), - ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: buildTitleBlock(), + ), + ], + ), + ), + ), ), ); } diff --git a/mobile/lib/modules/search/views/recently_added_page.dart b/mobile/lib/modules/search/views/recently_added_page.dart index fc079fbfc..538dea3d7 100644 --- a/mobile/lib/modules/search/views/recently_added_page.dart +++ b/mobile/lib/modules/search/views/recently_added_page.dart @@ -1,10 +1,10 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/recently_added.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; class RecentlyAddedPage extends HookConsumerWidget { const RecentlyAddedPage({super.key}); @@ -17,18 +17,14 @@ class RecentlyAddedPage extends HookConsumerWidget { appBar: AppBar( title: const Text('recently_added_page_title').tr(), leading: IconButton( - onPressed: () => AutoRouter.of(context).pop(), + onPressed: () => context.autoPop(), icon: const Icon(Icons.arrow_back_ios_rounded), ), ), - body: recents.when( - data: (searchResponse) => ImmichAssetGrid( + body: recents.widgetWhen( + onData: (searchResponse) => ImmichAssetGrid( assets: searchResponse, ), - error: (e, s) => Text(e.toString()), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), ), ); } diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart index 41984106d..fb4bd4979 100644 --- a/mobile/lib/modules/search/views/search_page.dart +++ b/mobile/lib/modules/search/views/search_page.dart @@ -1,9 +1,10 @@ import 'dart:math' as math; -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/search/models/curated_content.dart'; import 'package:immich_mobile/modules/search/providers/people.provider.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; @@ -15,7 +16,7 @@ import 'package:immich_mobile/modules/search/ui/search_row_title.dart'; import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; +import 'package:immich_mobile/shared/ui/scaffold_error_body.dart'; // ignore: must_be_immutable class SearchPage extends HookConsumerWidget { @@ -30,15 +31,14 @@ class SearchPage extends HookConsumerWidget { final curatedPeople = ref.watch(getCuratedPeopleProvider); final isMapEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.map)); - var isDarkTheme = Theme.of(context).brightness == Brightness.dark; - double imageSize = math.min(MediaQuery.of(context).size.width / 3, 150); + double imageSize = math.min(context.width / 3, 150); TextStyle categoryTitleStyle = const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14.0, + fontWeight: FontWeight.w500, + fontSize: 15.0, ); - Color categoryIconColor = isDarkTheme ? Colors.white : Colors.black; + Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black; useEffect( () { @@ -52,7 +52,7 @@ class SearchPage extends HookConsumerWidget { searchFocusNode.unfocus(); ref.watch(searchPageStateProvider.notifier).disableSearch(); - AutoRouter.of(context).push( + context.autoPush( SearchResultRoute( searchTerm: searchTerm, ), @@ -74,21 +74,12 @@ class SearchPage extends HookConsumerWidget { buildPeople() { return SizedBox( height: imageSize, - child: curatedPeople.when( - loading: () => const Center(child: ImmichLoadingIndicator()), - error: (err, stack) => Center(child: Text('Error: $err')), - data: (people) => CuratedPeopleRow( - content: people - .map( - (person) => CuratedContent( - id: person.id, - label: person.name, - ), - ) - .take(12) - .toList(), + child: curatedPeople.widgetWhen( + onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), + onData: (people) => CuratedPeopleRow( + content: people.take(12).toList(), onTap: (content, index) { - AutoRouter.of(context).push( + context.autoPush( PersonResultRoute( personId: content.id, personName: content.label, @@ -106,10 +97,9 @@ class SearchPage extends HookConsumerWidget { buildPlaces() { return SizedBox( height: imageSize, - child: curatedLocation.when( - loading: () => const Center(child: ImmichLoadingIndicator()), - error: (err, stack) => Center(child: Text('Error: $err')), - data: (locations) => CuratedPlacesRow( + child: curatedLocation.widgetWhen( + onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), + onData: (locations) => CuratedPlacesRow( isMapEnabled: isMapEnabled, content: locations .map( @@ -121,7 +111,7 @@ class SearchPage extends HookConsumerWidget { .toList(), imageSize: imageSize, onTap: (content, index) { - AutoRouter.of(context).push( + context.autoPush( SearchResultRoute( searchTerm: 'm:${content.label}', ), @@ -148,16 +138,14 @@ class SearchPage extends HookConsumerWidget { children: [ SearchRowTitle( title: "search_page_people".tr(), - onViewAllPressed: () => AutoRouter.of(context).push( - const AllPeopleRoute(), - ), + onViewAllPressed: () => + context.autoPush(const AllPeopleRoute()), ), buildPeople(), SearchRowTitle( title: "search_page_places".tr(), - onViewAllPressed: () => AutoRouter.of(context).push( - const CuratedLocationRoute(), - ), + onViewAllPressed: () => + context.autoPush(const CuratedLocationRoute()), top: 0, ), const SizedBox(height: 10.0), @@ -167,7 +155,9 @@ class SearchPage extends HookConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( 'search_page_your_activity', - style: Theme.of(context).textTheme.titleSmall, + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), ).tr(), ), ListTile( @@ -178,9 +168,7 @@ class SearchPage extends HookConsumerWidget { title: Text('search_page_favorites', style: categoryTitleStyle) .tr(), - onTap: () => AutoRouter.of(context).push( - const FavoritesRoute(), - ), + onTap: () => context.autoPush(const FavoritesRoute()), ), const CategoryDivider(), ListTile( @@ -192,25 +180,27 @@ class SearchPage extends HookConsumerWidget { 'search_page_recently_added', style: categoryTitleStyle, ).tr(), - onTap: () => AutoRouter.of(context).push( - const RecentlyAddedRoute(), - ), + onTap: () => context.autoPush(const RecentlyAddedRoute()), ), const SizedBox(height: 24.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Text( 'search_page_categories', - style: Theme.of(context).textTheme.titleSmall, + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), ).tr(), ), ListTile( - title: Text('Screenshots', style: categoryTitleStyle).tr(), + title: + Text('search_page_screenshots', style: categoryTitleStyle) + .tr(), leading: Icon( Icons.screenshot, color: categoryIconColor, ), - onTap: () => AutoRouter.of(context).push( + onTap: () => context.autoPush( SearchResultRoute( searchTerm: 'screenshots', ), @@ -224,7 +214,7 @@ class SearchPage extends HookConsumerWidget { Icons.photo_camera_front_outlined, color: categoryIconColor, ), - onTap: () => AutoRouter.of(context).push( + onTap: () => context.autoPush( SearchResultRoute( searchTerm: 'selfies', ), @@ -238,9 +228,7 @@ class SearchPage extends HookConsumerWidget { Icons.play_circle_outline, color: categoryIconColor, ), - onTap: () => AutoRouter.of(context).push( - const AllVideosRoute(), - ), + onTap: () => context.autoPush(const AllVideosRoute()), ), const CategoryDivider(), ListTile( @@ -252,9 +240,7 @@ class SearchPage extends HookConsumerWidget { Icons.motion_photos_on_outlined, color: categoryIconColor, ), - onTap: () => AutoRouter.of(context).push( - const AllMotionPhotosRoute(), - ), + onTap: () => context.autoPush(const AllMotionPhotosRoute()), ), ], ), @@ -274,7 +260,7 @@ class CategoryDivider extends StatelessWidget { Widget build(BuildContext context) { return const Padding( padding: EdgeInsets.only( - left: 72, + left: 56, right: 16, ), child: Divider( diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart index d6b1ea9a9..fd16c2c06 100644 --- a/mobile/lib/modules/search/views/search_result_page.dart +++ b/mobile/lib/modules/search/views/search_result_page.dart @@ -1,8 +1,8 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart'; @@ -38,7 +38,6 @@ class SearchResultPage extends HookConsumerWidget { final searchTermController = useTextEditingController(text: ""); final isNewSearch = useState(false); final currentSearchTerm = useState(searchTerm); - final isDarkTheme = Theme.of(context).brightness == Brightness.dark; final isDisplayDateGroup = useState(true); FocusNode? searchFocusNode; @@ -112,8 +111,9 @@ class SearchResultPage extends HookConsumerWidget { hintStyle: TextStyle( fontWeight: FontWeight.bold, fontSize: 16.0, - color: - isDarkTheme ? Colors.grey[500] : Colors.black.withOpacity(0.5), + color: context.isDarkTheme + ? Colors.grey[500] + : Colors.black.withOpacity(0.5), ), ), ); @@ -130,7 +130,7 @@ class SearchResultPage extends HookConsumerWidget { Text( currentSearchTerm.value, style: TextStyle( - color: Theme.of(context).primaryColor, + color: context.primaryColor, fontSize: 13, fontWeight: FontWeight.bold, ), @@ -138,12 +138,12 @@ class SearchResultPage extends HookConsumerWidget { ), Icon( Icons.close_rounded, - color: Theme.of(context).primaryColor, + color: context.primaryColor, size: 20, ), ], ), - backgroundColor: Theme.of(context).primaryColor.withAlpha(50), + backgroundColor: context.primaryColor.withAlpha(50), ); } @@ -185,7 +185,7 @@ class SearchResultPage extends HookConsumerWidget { if (isNewSearch.value) { isNewSearch.value = false; } else { - AutoRouter.of(context).pop(true); + context.autoPop(true); } }, icon: const Icon(Icons.arrow_back_ios_rounded), diff --git a/mobile/lib/modules/settings/providers/app_settings.provider.dart b/mobile/lib/modules/settings/providers/app_settings.provider.dart index f5d172e4c..96991451f 100644 --- a/mobile/lib/modules/settings/providers/app_settings.provider.dart +++ b/mobile/lib/modules/settings/providers/app_settings.provider.dart @@ -1,4 +1,8 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; -final appSettingsServiceProvider = Provider((ref) => AppSettingsService()); +part 'app_settings.provider.g.dart'; + +@Riverpod(keepAlive: true) +AppSettingsService appSettingsService(AppSettingsServiceRef ref) => + AppSettingsService(); diff --git a/mobile/lib/modules/settings/providers/app_settings.provider.g.dart b/mobile/lib/modules/settings/providers/app_settings.provider.g.dart new file mode 100644 index 000000000..692dcf7c0 --- /dev/null +++ b/mobile/lib/modules/settings/providers/app_settings.provider.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_settings.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$appSettingsServiceHash() => + r'957a65af6967701112f3076b507f9738fec4b7be'; + +/// See also [appSettingsService]. +@ProviderFor(appSettingsService) +final appSettingsServiceProvider = Provider.internal( + appSettingsService, + name: r'appSettingsServiceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$appSettingsServiceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef AppSettingsServiceRef = ProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/modules/settings/ui/advanced_settings/advanced_settings.dart b/mobile/lib/modules/settings/ui/advanced_settings/advanced_settings.dart index 3c8d47d1e..d0397fe5a 100644 --- a/mobile/lib/modules/settings/ui/advanced_settings/advanced_settings.dart +++ b/mobile/lib/modules/settings/ui/advanced_settings/advanced_settings.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; @@ -43,18 +44,13 @@ class AdvancedSettings extends HookConsumerWidget { final logLevel = Level.LEVELS[levelId.value].name; return ExpansionTile( - textColor: Theme.of(context).primaryColor, - title: const Text( + textColor: context.primaryColor, + title: Text( "advanced_settings_tile_title", - style: TextStyle( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleMedium, ).tr(), subtitle: const Text( "advanced_settings_tile_subtitle", - style: TextStyle( - fontSize: 13, - ), ).tr(), children: [ SettingsSwitchListTile( @@ -67,11 +63,10 @@ class AdvancedSettings extends HookConsumerWidget { ), ListTile( dense: true, - title: Text( - // Not translated because the levels are only English - "Log level: $logLevel", - style: const TextStyle(fontWeight: FontWeight.bold), - ), + title: const Text( + "advanced_settings_log_level_title", + style: TextStyle(fontWeight: FontWeight.bold), + ).tr(args: [logLevel]), subtitle: Slider( value: levelId.value.toDouble(), onChanged: (double v) => levelId.value = v.toInt(), @@ -86,7 +81,7 @@ class AdvancedSettings extends HookConsumerWidget { min: 1.0, divisions: 7, label: logLevel, - activeColor: Theme.of(context).primaryColor, + activeColor: context.primaryColor, ), ), SettingsSwitchListTile( diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart index 8ff719da3..aa3123f3d 100644 --- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart @@ -2,6 +2,7 @@ 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/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; @@ -50,13 +51,10 @@ class LayoutSettings extends HookConsumerWidget { return Column( children: [ SwitchListTile.adaptive( - activeColor: Theme.of(context).primaryColor, + activeColor: context.primaryColor, title: Text( "asset_list_layout_settings_dynamic_layout_title", - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith(fontWeight: FontWeight.bold), + style: context.textTheme.labelLarge, ).tr(), onChanged: switchChanged, value: useDynamicLayout.value, @@ -75,10 +73,10 @@ class LayoutSettings extends HookConsumerWidget { ).tr(), ), RadioListTile( - activeColor: Theme.of(context).primaryColor, + activeColor: context.primaryColor, title: Text( "asset_list_layout_settings_group_by_month_day", - style: Theme.of(context).textTheme.labelLarge, + style: context.textTheme.labelLarge, ).tr(), value: GroupAssetsBy.day, groupValue: groupBy.value, @@ -86,10 +84,10 @@ class LayoutSettings extends HookConsumerWidget { controlAffinity: ListTileControlAffinity.trailing, ), RadioListTile( - activeColor: Theme.of(context).primaryColor, + activeColor: context.primaryColor, title: Text( "asset_list_layout_settings_group_by_month", - style: Theme.of(context).textTheme.labelLarge, + style: context.textTheme.labelLarge, ).tr(), value: GroupAssetsBy.month, groupValue: groupBy.value, @@ -97,10 +95,10 @@ class LayoutSettings extends HookConsumerWidget { controlAffinity: ListTileControlAffinity.trailing, ), RadioListTile( - activeColor: Theme.of(context).primaryColor, + activeColor: context.primaryColor, title: Text( "asset_list_layout_settings_group_automatically", - style: Theme.of(context).textTheme.labelLarge, + style: context.textTheme.labelLarge, ).tr(), value: GroupAssetsBy.auto, groupValue: groupBy.value, diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_settings.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_settings.dart index ef7afb043..c3e0fc8eb 100644 --- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_settings.dart +++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_settings.dart @@ -1,5 +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/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart'; import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart'; import 'asset_list_tiles_per_row.dart'; @@ -12,18 +13,13 @@ class AssetListSettings extends StatelessWidget { @override Widget build(BuildContext context) { return ExpansionTile( - textColor: Theme.of(context).primaryColor, - title: const Text( + textColor: context.primaryColor, + title: Text( 'asset_list_settings_title', - style: TextStyle( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleMedium, ).tr(), subtitle: const Text( 'asset_list_settings_subtitle', - style: TextStyle( - fontSize: 13, - ), ).tr(), children: const [ TilesPerRow(), diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart index ae0e02148..b94635a30 100644 --- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart +++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart @@ -2,6 +2,7 @@ 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/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; @@ -33,13 +34,10 @@ class StorageIndicator extends HookConsumerWidget { ); return SwitchListTile.adaptive( - activeColor: Theme.of(context).primaryColor, + activeColor: context.primaryColor, title: Text( "theme_setting_asset_list_storage_indicator_title", - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith(fontWeight: FontWeight.bold), + style: context.textTheme.labelLarge, ).tr(), onChanged: switchChanged, value: showStorageIndicator.value, diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart index 89ac79133..e2f245582 100644 --- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart +++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart @@ -2,6 +2,7 @@ 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/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; @@ -36,12 +37,9 @@ class TilesPerRow extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( - title: const Text( + title: Text( "theme_setting_asset_list_tiles_per_row_title", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - ), + style: context.textTheme.labelLarge, ).tr(args: ["${itemsValue.value.toInt()}"]), ), Slider( @@ -51,7 +49,7 @@ class TilesPerRow extends HookConsumerWidget { max: 6, divisions: 4, label: "${itemsValue.value.toInt()}", - activeColor: Theme.of(context).primaryColor, + activeColor: context.primaryColor, ), ], ); diff --git a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart index 123c7cd00..5c8620473 100644 --- a/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart +++ b/mobile/lib/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart @@ -2,6 +2,7 @@ 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/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart'; @@ -26,23 +27,20 @@ class ImageViewerQualitySetting extends HookConsumerWidget { ); return ExpansionTile( - textColor: Theme.of(context).primaryColor, - title: const Text( + textColor: context.primaryColor, + title: Text( 'theme_setting_image_viewer_quality_title', - style: TextStyle( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleMedium, ).tr(), subtitle: const Text( 'theme_setting_image_viewer_quality_subtitle', - style: TextStyle( - fontSize: 13, - ), ).tr(), children: [ ListTile( - title: const Text('setting_image_viewer_help').tr(), - dense: true, + title: Text( + 'setting_image_viewer_help', + style: context.textTheme.bodyMedium, + ).tr(), ), SettingsSwitchListTile( appSettingService: settings, diff --git a/mobile/lib/modules/settings/ui/local_storage_settings/local_storage_settings.dart b/mobile/lib/modules/settings/ui/local_storage_settings/local_storage_settings.dart index a9321a56e..a64da0481 100644 --- a/mobile/lib/modules/settings/ui/local_storage_settings/local_storage_settings.dart +++ b/mobile/lib/modules/settings/ui/local_storage_settings/local_storage_settings.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; @@ -25,38 +26,27 @@ class LocalStorageSettings extends HookConsumerWidget { } return ExpansionTile( - textColor: Theme.of(context).primaryColor, - title: const Text( + textColor: context.primaryColor, + title: Text( "cache_settings_tile_title", - style: TextStyle( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleMedium, ).tr(), subtitle: const Text( "cache_settings_tile_subtitle", - style: TextStyle( - fontSize: 13, - ), ).tr(), children: [ ListTile( title: Text( - "Duplicated Assets (${cacheItemCount.value})", - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith(fontWeight: FontWeight.bold), - ).tr(), + "cache_settings_duplicated_assets_title", + style: context.textTheme.titleSmall, + ).tr(args: ["${cacheItemCount.value}"]), subtitle: const Text( - "Photos and videos that are black listed by the app", - style: TextStyle( - fontSize: 13, - ), + "cache_settings_duplicated_assets_subtitle", ).tr(), trailing: TextButton( onPressed: cacheItemCount.value > 0 ? clearCache : null, child: Text( - "CLEAR", + "cache_settings_duplicated_assets_clear_button", style: TextStyle( fontSize: 12, color: cacheItemCount.value > 0 ? Colors.red : Colors.grey, diff --git a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart b/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart index 5f00cd0d9..e996ad1ea 100644 --- a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart +++ b/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart @@ -2,6 +2,7 @@ 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/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/providers/notification_permission.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; @@ -49,12 +50,12 @@ class NotificationSetting extends HookConsumerWidget { actions: [ TextButton( child: const Text('notification_permission_dialog_cancel').tr(), - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.pop(), ), TextButton( child: const Text('notification_permission_dialog_settings').tr(), onPressed: () { - Navigator.of(context).pop(); + context.pop(); openAppSettings(); }, ), @@ -65,18 +66,13 @@ class NotificationSetting extends HookConsumerWidget { final String formattedValue = _formatSliderValue(sliderValue.value); return ExpansionTile( - textColor: Theme.of(context).primaryColor, - title: const Text( + textColor: context.primaryColor, + title: Text( 'setting_notifications_title', - style: TextStyle( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleMedium, ).tr(), subtitle: const Text( 'setting_notifications_subtitle', - style: TextStyle( - fontSize: 13, - ), ).tr(), children: [ if (!hasPermission) @@ -84,9 +80,7 @@ class NotificationSetting extends HookConsumerWidget { leading: const Icon(Icons.notifications_outlined), title: Text( 'notification_permission_list_tile_title', - style: Theme.of(context) - .textTheme - .labelLarge + style: context.textTheme.labelLarge ?.copyWith(fontWeight: FontWeight.bold), ).tr(), subtitle: Column( @@ -94,7 +88,7 @@ class NotificationSetting extends HookConsumerWidget { children: [ Text( 'notification_permission_list_tile_content', - style: Theme.of(context).textTheme.labelMedium, + style: context.textTheme.labelMedium, ).tr(), const SizedBox(height: 8), ElevatedButton( @@ -149,7 +143,7 @@ class NotificationSetting extends HookConsumerWidget { max: 5.0, divisions: 5, label: formattedValue, - activeColor: Theme.of(context).primaryColor, + activeColor: context.primaryColor, ), ), ], diff --git a/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart b/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart index c6fff19f6..b5277b9c1 100644 --- a/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart +++ b/mobile/lib/modules/settings/ui/settings_switch_list_tile.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; class SettingsSwitchListTile extends StatelessWidget { @@ -23,7 +24,7 @@ class SettingsSwitchListTile extends StatelessWidget { @override Widget build(BuildContext context) { return SwitchListTile.adaptive( - selectedTileColor: enabled ? null : Theme.of(context).disabledColor, + selectedTileColor: enabled ? null : context.themeData.disabledColor, value: valueNotifier.value, onChanged: (bool value) { if (enabled) { @@ -34,18 +35,19 @@ class SettingsSwitchListTile extends StatelessWidget { onChanged!(value); } }, - activeColor: enabled - ? Theme.of(context).primaryColor - : Theme.of(context).disabledColor, + activeColor: + enabled ? context.primaryColor : context.themeData.disabledColor, dense: true, title: Text( title, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith(fontWeight: FontWeight.bold), + style: context.textTheme.titleSmall, ), - subtitle: subtitle != null ? Text(subtitle!) : null, + subtitle: subtitle != null + ? Text( + subtitle!, + style: context.textTheme.bodyMedium, + ) + : null, ); } } diff --git a/mobile/lib/modules/settings/ui/theme_setting/theme_setting.dart b/mobile/lib/modules/settings/ui/theme_setting/theme_setting.dart index 2e3b7134c..0657fa7e0 100644 --- a/mobile/lib/modules/settings/ui/theme_setting/theme_setting.dart +++ b/mobile/lib/modules/settings/ui/theme_setting/theme_setting.dart @@ -2,6 +2,7 @@ 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/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/utils/immich_app_theme.dart'; @@ -24,27 +25,20 @@ class ThemeSetting extends HookConsumerWidget { ); return ExpansionTile( - textColor: Theme.of(context).primaryColor, - title: const Text( + textColor: context.primaryColor, + title: Text( 'theme_setting_theme_title', - style: TextStyle( - fontWeight: FontWeight.bold, - ), + style: context.textTheme.titleMedium, ).tr(), subtitle: const Text( 'theme_setting_theme_subtitle', - style: TextStyle( - fontSize: 13, - ), ).tr(), children: [ SwitchListTile.adaptive( - activeColor: Theme.of(context).primaryColor, + activeColor: context.primaryColor, title: Text( 'theme_setting_system_theme_switch', - style: Theme.of(context) - .textTheme - .labelLarge + style: context.textTheme.labelLarge ?.copyWith(fontWeight: FontWeight.bold), ).tr(), value: currentTheme.value == ThemeMode.system, @@ -77,12 +71,10 @@ class ThemeSetting extends HookConsumerWidget { ), if (currentTheme.value != ThemeMode.system) SwitchListTile.adaptive( - activeColor: Theme.of(context).primaryColor, + activeColor: context.primaryColor, title: Text( 'theme_setting_dark_mode_switch', - style: Theme.of(context) - .textTheme - .labelLarge + style: context.textTheme.labelLarge ?.copyWith(fontWeight: FontWeight.bold), ).tr(), value: ref.watch(immichThemeProvider) == ThemeMode.dark, diff --git a/mobile/lib/modules/settings/views/settings_page.dart b/mobile/lib/modules/settings/views/settings_page.dart index e0547d0d6..0a1651de9 100644 --- a/mobile/lib/modules/settings/views/settings_page.dart +++ b/mobile/lib/modules/settings/views/settings_page.dart @@ -1,6 +1,7 @@ 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/modules/settings/ui/advanced_settings/advanced_settings.dart'; import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart'; import 'package:immich_mobile/modules/settings/ui/local_storage_settings/local_storage_settings.dart'; @@ -18,19 +19,13 @@ class SettingsPage extends HookConsumerWidget { leading: IconButton( iconSize: 20, splashRadius: 24, - onPressed: () { - Navigator.pop(context); - }, + onPressed: () => context.pop(), icon: const Icon(Icons.arrow_back_ios_new_rounded), ), automaticallyImplyLeading: false, centerTitle: false, title: const Text( 'setting_pages_app_bar_settings', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), ).tr(), ), body: ListView( diff --git a/mobile/lib/modules/shared_link/ui/shared_link_item.dart b/mobile/lib/modules/shared_link/ui/shared_link_item.dart index 907006f77..8605bdeaf 100644 --- a/mobile/lib/modules/shared_link/ui/shared_link_item.dart +++ b/mobile/lib/modules/shared_link/ui/shared_link_item.dart @@ -1,9 +1,10 @@ import 'dart:math' as math; -import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart'; import 'package:immich_mobile/modules/shared_link/models/shared_link.dart'; import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart'; @@ -26,13 +27,13 @@ class SharedLinkItem extends ConsumerWidget { } Widget getExpiryDuration(bool isDarkMode) { - var expiresText = "Expires ∞"; + var expiresText = "shared_link_expires_never".tr(); if (sharedLink.expiresAt != null) { if (isExpired()) { return Text( - "Expired", + "shared_link_expired", style: TextStyle(color: Colors.red[300]), - ); + ).tr(); } final difference = sharedLink.expiresAt!.difference(DateTime.now()); debugPrint("Difference: $difference"); @@ -41,13 +42,15 @@ class SharedLinkItem extends ConsumerWidget { if (difference.inHours % 24 > 12) { dayDifference += 1; } - expiresText = "in $dayDifference days"; + expiresText = "shared_link_expires_days".plural(dayDifference); } else if (difference.inHours > 0) { - expiresText = "in ${difference.inHours} hours"; + expiresText = "shared_link_expires_hours".plural(difference.inHours); } else if (difference.inMinutes > 0) { - expiresText = "in ${difference.inMinutes} minutes"; + expiresText = + "shared_link_expires_minutes".plural(difference.inMinutes); } else if (difference.inSeconds > 0) { - expiresText = "in ${difference.inSeconds} seconds"; + expiresText = + "shared_link_expires_seconds".plural(difference.inSeconds); } } return Text( @@ -58,12 +61,12 @@ class SharedLinkItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final themeData = Theme.of(context); + final themeData = context.themeData; final isDarkMode = themeData.brightness == Brightness.dark; final thumbnailUrl = sharedLink.thumbAssetId != null ? getThumbnailUrlForRemoteId(sharedLink.thumbAssetId!) : null; - final imageSize = math.min(MediaQuery.of(context).size.width / 4, 100.0); + final imageSize = math.min(context.width / 4, 100.0); void copyShareLinkToClipboard() { final serverUrl = getServerUrl(); @@ -72,7 +75,7 @@ class SharedLinkItem extends ConsumerWidget { context: context, gravity: ToastGravity.BOTTOM, toastType: ToastType.error, - msg: 'Cannot fetch the server url', + msg: "shared_link_error_server_url_fetch".tr(), ); return; } @@ -83,11 +86,14 @@ class SharedLinkItem extends ConsumerWidget { ), ).then((_) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( + SnackBar( content: Text( - "Copied to clipboard", - ), - duration: Duration(seconds: 2), + "shared_link_clipboard_copied_massage", + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ).tr(), + duration: const Duration(seconds: 2), ), ); }); @@ -148,8 +154,8 @@ class SharedLinkItem extends ConsumerWidget { label: Text( labelText, style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, + fontSize: 11, + fontWeight: FontWeight.w500, color: isDarkMode ? Colors.black : Colors.white, ), ), @@ -163,9 +169,12 @@ class SharedLinkItem extends ConsumerWidget { Widget buildBottomInfo() { return Row( children: [ - if (sharedLink.allowUpload) buildInfoChip("Upload"), - if (sharedLink.allowDownload) buildInfoChip("Download"), - if (sharedLink.showMetadata) buildInfoChip("EXIF"), + if (sharedLink.allowUpload) + buildInfoChip("shared_link_info_chip_upload".tr()), + if (sharedLink.allowDownload) + buildInfoChip("shared_link_info_chip_download".tr()), + if (sharedLink.showMetadata) + buildInfoChip("shared_link_info_chip_metadata".tr()), ], ); } @@ -194,8 +203,8 @@ class SharedLinkItem extends ConsumerWidget { tapTargetSize: MaterialTapTargetSize.shrinkWrap, // the '2023' part ), - onPressed: () => AutoRouter.of(context) - .push(SharedLinkEditRoute(existingLink: sharedLink)), + onPressed: () => + context.autoPush(SharedLinkEditRoute(existingLink: sharedLink)), ), IconButton( splashRadius: 25, diff --git a/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart b/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart index 499b2c29d..fe2212cc2 100644 --- a/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart +++ b/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart @@ -1,10 +1,10 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/shared_link/models/shared_link.dart'; import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart'; import 'package:immich_mobile/modules/shared_link/services/shared_link.service.dart'; @@ -26,7 +26,7 @@ class SharedLinkEditPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { const padding = 20.0; - final themeData = Theme.of(context); + final themeData = context.themeData; final descriptionController = useTextEditingController(text: existingLink?.description ?? ""); final descriptionFocusNode = useFocusNode(); @@ -206,9 +206,16 @@ class SharedLinkEditPage extends HookConsumerWidget { Widget buildExpiryAfterButton() { return DropdownMenu( + label: Text( + "shared_link_edit_expire_after", + style: TextStyle( + fontWeight: FontWeight.bold, + color: themeData.primaryColor, + ), + ).tr(), enableSearch: false, enableFilter: false, - width: MediaQuery.of(context).size.width - 40, + width: context.width - 40, initialSelection: expiryAfter.value, enabled: newShareLink.value.isEmpty && (existingLink == null || editExpiry.value), @@ -223,31 +230,34 @@ class SharedLinkEditPage extends HookConsumerWidget { borderSide: BorderSide(color: Colors.grey), ), ), - dropdownMenuEntries: const [ - DropdownMenuEntry(value: 0, label: "Never"), + dropdownMenuEntries: [ + DropdownMenuEntry( + value: 0, + label: "shared_link_edit_expire_after_option_never".tr(), + ), DropdownMenuEntry( value: 30, - label: '30 minutes', + label: "shared_link_edit_expire_after_option_minutes".plural(30), ), DropdownMenuEntry( value: 60, - label: '1 hour', + label: "shared_link_edit_expire_after_option_hours".plural(1), ), DropdownMenuEntry( value: 60 * 6, - label: '6 hours', + label: "shared_link_edit_expire_after_option_hours".plural(6), ), DropdownMenuEntry( value: 60 * 24, - label: '1 day', + label: "shared_link_edit_expire_after_option_days".plural(1), ), DropdownMenuEntry( value: 60 * 24 * 7, - label: '7 days', + label: "shared_link_edit_expire_after_option_days".plural(7), ), DropdownMenuEntry( value: 60 * 24 * 30, - label: '30 days', + label: "shared_link_edit_expire_after_option_days".plural(30), ), ], ); @@ -258,15 +268,20 @@ class SharedLinkEditPage extends HookConsumerWidget { ClipboardData( text: passwordController.text.isEmpty ? newShareLink.value - : "Link: ${newShareLink.value}\nPassword: ${passwordController.text}", + : "shared_link_clipboard_text".tr( + args: [newShareLink.value, passwordController.text], + ), ), ).then((_) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( + SnackBar( content: Text( - "Copied to clipboard", - ), - duration: Duration(seconds: 2), + "shared_link_clipboard_copied_massage", + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ).tr(), + duration: const Duration(seconds: 2), ), ); }); @@ -300,10 +315,10 @@ class SharedLinkEditPage extends HookConsumerWidget { alignment: Alignment.bottomRight, child: ElevatedButton( onPressed: () { - AutoRouter.of(context).pop(); + context.autoPop(); }, child: const Text( - "Done", + "share_done", style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -346,7 +361,7 @@ class SharedLinkEditPage extends HookConsumerWidget { context: context, gravity: ToastGravity.BOTTOM, toastType: ToastType.error, - msg: 'Error while creating shared link', + msg: 'shared_link_create_error'.tr(), ); } } @@ -396,7 +411,7 @@ class SharedLinkEditPage extends HookConsumerWidget { changeExpiry: changeExpiry, ); ref.invalidate(sharedLinksStateProvider); - AutoRouter.of(context).pop(); + context.autoPop(); } return Scaffold( diff --git a/mobile/lib/modules/shared_link/views/shared_link_page.dart b/mobile/lib/modules/shared_link/views/shared_link_page.dart index 19bede4bd..7638441b1 100644 --- a/mobile/lib/modules/shared_link/views/shared_link_page.dart +++ b/mobile/lib/modules/shared_link/views/shared_link_page.dart @@ -2,10 +2,11 @@ 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/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/shared_link/models/shared_link.dart'; import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart'; import 'package:immich_mobile/modules/shared_link/ui/shared_link_item.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; class SharedLinkPage extends HookConsumerWidget { const SharedLinkPage({Key? key}) : super(key: key); @@ -17,7 +18,10 @@ class SharedLinkPage extends HookConsumerWidget { useEffect( () { ref.read(sharedLinksStateProvider.notifier).fetchLinks(); - return () => ref.invalidate(sharedLinksStateProvider); + return () { + if (!context.mounted) return; + ref.invalidate(sharedLinksStateProvider); + }; }, [], ); @@ -52,7 +56,7 @@ class SharedLinkPage extends HookConsumerWidget { child: Icon( Icons.link_off, size: 100, - color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + color: context.themeData.iconTheme.color?.withOpacity(0.5), ), ), ), @@ -66,12 +70,10 @@ class SharedLinkPage extends HookConsumerWidget { children: [ Padding( padding: const EdgeInsets.only(left: 16.0, top: 16.0, bottom: 30.0), - child: const Text( + child: Text( "shared_link_manage_links", - style: TextStyle( - fontSize: 14, - color: Colors.grey, - fontWeight: FontWeight.bold, + style: context.textTheme.labelLarge?.copyWith( + color: context.textTheme.labelLarge?.color?.withAlpha(200), ), ).tr(), ), @@ -114,11 +116,10 @@ class SharedLinkPage extends HookConsumerWidget { centerTitle: false, ), body: SafeArea( - child: sharedLinks.when( - data: (links) => + child: sharedLinks.widgetWhen( + onError: (error, stackTrace) => buildNoShares(), + onData: (links) => links.isNotEmpty ? buildSharesList(links) : buildNoShares(), - error: (error, stackTrace) => buildNoShares(), - loading: () => const Center(child: ImmichLoadingIndicator()), ), ), ); diff --git a/mobile/lib/modules/trash/providers/trashed_asset.provider.dart b/mobile/lib/modules/trash/providers/trashed_asset.provider.dart index 2d85625a4..04f8d5f16 100644 --- a/mobile/lib/modules/trash/providers/trashed_asset.provider.dart +++ b/mobile/lib/modules/trash/providers/trashed_asset.provider.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/services/sync.service.dart'; +import 'package:immich_mobile/utils/renderlist_generator.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; @@ -37,6 +38,7 @@ class TrashNotifier extends StateNotifier { .remoteIdProperty() .findAll(); + // TODO: handle local asset removal on emptyTrash _ref .read(syncServiceProvider) .handleRemoteAssetRemoval(idsToRemove.cast().toList()); @@ -106,9 +108,9 @@ final trashProvider = StateNotifierProvider((ref) { ); }); -final trashedAssetsProvider = StreamProvider((ref) async* { +final trashedAssetsProvider = StreamProvider((ref) { final user = ref.read(currentUserProvider); - if (user == null) return; + if (user == null) return const Stream.empty(); final query = ref .watch(dbProvider) .assets @@ -116,9 +118,5 @@ final trashedAssetsProvider = StreamProvider((ref) async* { .ownerIdEqualTo(user.isarId) .isTrashedEqualTo(true) .sortByFileCreatedAt(); - const groupBy = GroupAssetsBy.none; - yield await RenderList.fromQuery(query, groupBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupBy); - } + return renderListGeneratorWithGroupBy(query, GroupAssetsBy.none); }); diff --git a/mobile/lib/modules/trash/views/trash_page.dart b/mobile/lib/modules/trash/views/trash_page.dart index 8c128b61d..88fd32d01 100644 --- a/mobile/lib/modules/trash/views/trash_page.dart +++ b/mobile/lib/modules/trash/views/trash_page.dart @@ -1,9 +1,10 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/home/ui/delete_dialog.dart'; import 'package:immich_mobile/modules/trash/providers/trashed_asset.provider.dart'; @@ -11,8 +12,8 @@ import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; +import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; class TrashPage extends HookConsumerWidget { const TrashPage({super.key}); @@ -24,7 +25,7 @@ class TrashPage extends HookConsumerWidget { ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays)); final selectionEnabledHook = useState(false); final selection = useState({}); - final processing = useState(false); + final processing = useProcessingOverlay(); void selectionListener( bool multiselect, @@ -137,7 +138,7 @@ class TrashPage extends HookConsumerWidget { return AppBar( leading: IconButton( onPressed: !selectionEnabledHook.value - ? () => AutoRouter.of(context).pop() + ? () => context.autoPop() : () { selectionEnabledHook.value = false; selection.value = {}; @@ -177,7 +178,7 @@ class TrashPage extends HookConsumerWidget { child: SizedBox( height: 64, child: Container( - color: Theme.of(context).canvasColor, + color: context.themeData.canvasColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -229,18 +230,13 @@ class TrashPage extends HookConsumerWidget { ); } - return trashedAssets.when( - loading: () => Scaffold( - appBar: buildAppBar("?"), - body: const Center(child: CircularProgressIndicator()), + return Scaffold( + appBar: trashedAssets.maybeWhen( + orElse: () => buildAppBar("?"), + data: (data) => buildAppBar(data.totalAssets.toString()), ), - error: (error, stackTrace) => Scaffold( - appBar: buildAppBar("!"), - body: Center(child: Text(error.toString())), - ), - data: (data) => Scaffold( - appBar: buildAppBar(data.totalAssets.toString()), - body: data.isEmpty + body: trashedAssets.widgetWhen( + onData: (data) => data.isEmpty ? Center( child: Text('trash_page_no_assets'.tr()), ) @@ -254,11 +250,9 @@ class TrashPage extends HookConsumerWidget { showMultiSelectIndicator: false, showStack: true, topWidget: Padding( - padding: const EdgeInsets.only( - top: 24, - bottom: 24, - left: 12, - right: 12, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 24, ), child: const Text( "trash_page_info", @@ -267,8 +261,6 @@ class TrashPage extends HookConsumerWidget { ), ), if (selectionEnabledHook.value) buildBottomBar(), - if (processing.value) - const Center(child: ImmichLoadingIndicator()), ], ), ), diff --git a/mobile/lib/routing/gallery_permission_guard.dart b/mobile/lib/routing/backup_permission_guard.dart similarity index 73% rename from mobile/lib/routing/gallery_permission_guard.dart rename to mobile/lib/routing/backup_permission_guard.dart index 65455584b..81d40513b 100644 --- a/mobile/lib/routing/gallery_permission_guard.dart +++ b/mobile/lib/routing/backup_permission_guard.dart @@ -2,10 +2,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -class GalleryPermissionGuard extends AutoRouteGuard { +class BackupPermissionGuard extends AutoRouteGuard { final GalleryPermissionNotifier _permission; - GalleryPermissionGuard(this._permission); + BackupPermissionGuard(this._permission); @override void onNavigation(NavigationResolver resolver, StackRouter router) async { @@ -13,7 +13,7 @@ class GalleryPermissionGuard extends AutoRouteGuard { if (p) { resolver.next(true); } else { - router.replaceAll([const PermissionOnboardingRoute()]); + router.push(const PermissionOnboardingRoute()); } } } diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 01d54082e..dfb87a4ae 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -44,7 +44,7 @@ import 'package:immich_mobile/modules/search/views/search_result_page.dart'; import 'package:immich_mobile/modules/settings/views/settings_page.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; import 'package:immich_mobile/routing/duplicate_guard.dart'; -import 'package:immich_mobile/routing/gallery_permission_guard.dart'; +import 'package:immich_mobile/routing/backup_permission_guard.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/logger_message.model.dart'; @@ -77,7 +77,7 @@ part 'router.gr.dart'; AutoRoute(page: ChangePasswordPage), CustomRoute( page: TabControllerPage, - guards: [AuthGuard, DuplicateGuard, GalleryPermissionGuard], + guards: [AuthGuard, DuplicateGuard], children: [ AutoRoute(page: HomePage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: SearchPage, guards: [AuthGuard, DuplicateGuard]), @@ -88,10 +88,13 @@ part 'router.gr.dart'; ), AutoRoute( page: GalleryViewerPage, - guards: [AuthGuard, DuplicateGuard, GalleryPermissionGuard], + guards: [AuthGuard, DuplicateGuard], ), AutoRoute(page: VideoViewerPage, guards: [AuthGuard, DuplicateGuard]), - AutoRoute(page: BackupControllerPage, guards: [AuthGuard, DuplicateGuard]), + AutoRoute( + page: BackupControllerPage, + guards: [AuthGuard, DuplicateGuard, BackupPermissionGuard], + ), AutoRoute(page: SearchResultPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: CuratedLocationPage, guards: [AuthGuard, DuplicateGuard]), AutoRoute(page: CreateAlbumPage, guards: [AuthGuard, DuplicateGuard]), @@ -179,8 +182,8 @@ class AppRouter extends _$AppRouter { ) : super( authGuard: AuthGuard(_apiService), duplicateGuard: DuplicateGuard(), - galleryPermissionGuard: - GalleryPermissionGuard(galleryPermissionNotifier), + backupPermissionGuard: + BackupPermissionGuard(galleryPermissionNotifier), ); } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 885b55643..583aa1125 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -17,14 +17,14 @@ class _$AppRouter extends RootStackRouter { GlobalKey? navigatorKey, required this.authGuard, required this.duplicateGuard, - required this.galleryPermissionGuard, + required this.backupPermissionGuard, }) : super(navigatorKey); final AuthGuard authGuard; final DuplicateGuard duplicateGuard; - final GalleryPermissionGuard galleryPermissionGuard; + final BackupPermissionGuard backupPermissionGuard; @override final Map pagesMap = { @@ -348,6 +348,7 @@ class _$AppRouter extends RootStackRouter { assetId: args.assetId, withAssetThumbs: args.withAssetThumbs, isOwner: args.isOwner, + isReadOnly: args.isReadOnly, key: args.key, ), transitionsBuilder: TransitionsBuilders.slideLeft, @@ -413,7 +414,6 @@ class _$AppRouter extends RootStackRouter { guards: [ authGuard, duplicateGuard, - galleryPermissionGuard, ], children: [ RouteConfig( @@ -460,7 +460,6 @@ class _$AppRouter extends RootStackRouter { guards: [ authGuard, duplicateGuard, - galleryPermissionGuard, ], ), RouteConfig( @@ -477,6 +476,7 @@ class _$AppRouter extends RootStackRouter { guards: [ authGuard, duplicateGuard, + backupPermissionGuard, ], ), RouteConfig( @@ -1568,6 +1568,7 @@ class ActivitiesRoute extends PageRouteInfo { String? assetId, bool withAssetThumbs = true, bool isOwner = false, + bool isReadOnly = false, Key? key, }) : super( ActivitiesRoute.name, @@ -1578,6 +1579,7 @@ class ActivitiesRoute extends PageRouteInfo { assetId: assetId, withAssetThumbs: withAssetThumbs, isOwner: isOwner, + isReadOnly: isReadOnly, key: key, ), ); @@ -1592,6 +1594,7 @@ class ActivitiesRouteArgs { this.assetId, this.withAssetThumbs = true, this.isOwner = false, + this.isReadOnly = false, this.key, }); @@ -1605,11 +1608,13 @@ class ActivitiesRouteArgs { final bool isOwner; + final bool isReadOnly; + final Key? key; @override String toString() { - return 'ActivitiesRouteArgs{albumId: $albumId, appBarTitle: $appBarTitle, assetId: $assetId, withAssetThumbs: $withAssetThumbs, isOwner: $isOwner, key: $key}'; + return 'ActivitiesRouteArgs{albumId: $albumId, appBarTitle: $appBarTitle, assetId: $assetId, withAssetThumbs: $withAssetThumbs, isOwner: $isOwner, isReadOnly: $isReadOnly, key: $key}'; } } diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart index 5a4ee4398..dafbedd31 100644 --- a/mobile/lib/routing/tab_navigation_observer.dart +++ b/mobile/lib/routing/tab_navigation_observer.dart @@ -63,7 +63,10 @@ class TabNavigationObserver extends AutoRouterObserver { return; } - Store.put(StoreKey.currentUser, User.fromDto(userResponseDto)); + Store.put( + StoreKey.currentUser, + User.fromUserDto(userResponseDto), + ); ref.read(serverInfoProvider.notifier).getServerVersion(); } catch (e) { debugPrint("Error refreshing user info $e"); diff --git a/mobile/lib/shared/models/album.dart b/mobile/lib/shared/models/album.dart index f338abca3..26b1ab16e 100644 --- a/mobile/lib/shared/models/album.dart +++ b/mobile/lib/shared/models/album.dart @@ -22,6 +22,7 @@ class Album { this.endDate, this.lastModifiedAssetTimestamp, required this.shared, + required this.activityEnabled, }); Id id = Isar.autoIncrement; @@ -36,6 +37,7 @@ class Album { DateTime? endDate; DateTime? lastModifiedAssetTimestamp; bool shared; + bool activityEnabled; final IsarLink owner = IsarLink(); final IsarLink thumbnail = IsarLink(); final IsarLinks sharedUsers = IsarLinks(); @@ -66,18 +68,16 @@ class Album { } final name = []; - if (owner.value?.firstName != null) { - name.add(owner.value!.firstName); - } - if (owner.value?.lastName != null) { - name.add(owner.value!.lastName); + if (owner.value?.name != null) { + name.add(owner.value!.name); } return name.join(' '); } Stream watchRenderList(GroupAssetsBy groupAssetsBy) async* { - final query = assets.filter().sortByFileCreatedAtDesc(); + final query = + assets.filter().isTrashedEqualTo(false).sortByFileCreatedAtDesc(); _renderList = await RenderList.fromQuery(query, groupAssetsBy); yield _renderList; await for (final _ in query.watchLazy()) { @@ -105,6 +105,7 @@ class Album { modifiedAt.isAtSameMomentAs(other.modifiedAt) && lastModifiedAssetTimestampIsSetAndEqual && shared == other.shared && + activityEnabled == other.activityEnabled && owner.value == other.owner.value && thumbnail.value == other.thumbnail.value && sharedUsers.length == other.sharedUsers.length && @@ -122,6 +123,7 @@ class Album { modifiedAt.hashCode ^ lastModifiedAssetTimestamp.hashCode ^ shared.hashCode ^ + activityEnabled.hashCode ^ owner.value.hashCode ^ thumbnail.value.hashCode ^ sharedUsers.length.hashCode ^ @@ -133,6 +135,7 @@ class Album { createdAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(), modifiedAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(), shared: false, + activityEnabled: false, ); a.owner.value = Store.get(StoreKey.currentUser); a.localId = ape.id; @@ -150,6 +153,7 @@ class Album { shared: dto.shared, startDate: dto.startDate, endDate: dto.endDate, + activityEnabled: dto.isActivityEnabled, ); a.owner.value = await db.users.getById(dto.ownerId); if (dto.albumThumbnailAssetId != null) { diff --git a/mobile/lib/shared/models/album.g.dart b/mobile/lib/shared/models/album.g.dart index 9cdb59a5e..e9fcc49aa 100644 --- a/mobile/lib/shared/models/album.g.dart +++ b/mobile/lib/shared/models/album.g.dart @@ -17,48 +17,53 @@ const AlbumSchema = CollectionSchema( name: r'Album', id: -1355968412107120937, properties: { - r'createdAt': PropertySchema( + r'activityEnabled': PropertySchema( id: 0, + name: r'activityEnabled', + type: IsarType.bool, + ), + r'createdAt': PropertySchema( + id: 1, name: r'createdAt', type: IsarType.dateTime, ), r'endDate': PropertySchema( - id: 1, + id: 2, name: r'endDate', type: IsarType.dateTime, ), r'lastModifiedAssetTimestamp': PropertySchema( - id: 2, + id: 3, name: r'lastModifiedAssetTimestamp', type: IsarType.dateTime, ), r'localId': PropertySchema( - id: 3, + id: 4, name: r'localId', type: IsarType.string, ), r'modifiedAt': PropertySchema( - id: 4, + id: 5, name: r'modifiedAt', type: IsarType.dateTime, ), r'name': PropertySchema( - id: 5, + id: 6, name: r'name', type: IsarType.string, ), r'remoteId': PropertySchema( - id: 6, + id: 7, name: r'remoteId', type: IsarType.string, ), r'shared': PropertySchema( - id: 7, + id: 8, name: r'shared', type: IsarType.bool, ), r'startDate': PropertySchema( - id: 8, + id: 9, name: r'startDate', type: IsarType.dateTime, ) @@ -157,15 +162,16 @@ void _albumSerialize( List offsets, Map> allOffsets, ) { - writer.writeDateTime(offsets[0], object.createdAt); - writer.writeDateTime(offsets[1], object.endDate); - writer.writeDateTime(offsets[2], object.lastModifiedAssetTimestamp); - writer.writeString(offsets[3], object.localId); - writer.writeDateTime(offsets[4], object.modifiedAt); - writer.writeString(offsets[5], object.name); - writer.writeString(offsets[6], object.remoteId); - writer.writeBool(offsets[7], object.shared); - writer.writeDateTime(offsets[8], object.startDate); + writer.writeBool(offsets[0], object.activityEnabled); + writer.writeDateTime(offsets[1], object.createdAt); + writer.writeDateTime(offsets[2], object.endDate); + writer.writeDateTime(offsets[3], object.lastModifiedAssetTimestamp); + writer.writeString(offsets[4], object.localId); + writer.writeDateTime(offsets[5], object.modifiedAt); + writer.writeString(offsets[6], object.name); + writer.writeString(offsets[7], object.remoteId); + writer.writeBool(offsets[8], object.shared); + writer.writeDateTime(offsets[9], object.startDate); } Album _albumDeserialize( @@ -175,15 +181,16 @@ Album _albumDeserialize( Map> allOffsets, ) { final object = Album( - createdAt: reader.readDateTime(offsets[0]), - endDate: reader.readDateTimeOrNull(offsets[1]), - lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[2]), - localId: reader.readStringOrNull(offsets[3]), - modifiedAt: reader.readDateTime(offsets[4]), - name: reader.readString(offsets[5]), - remoteId: reader.readStringOrNull(offsets[6]), - shared: reader.readBool(offsets[7]), - startDate: reader.readDateTimeOrNull(offsets[8]), + activityEnabled: reader.readBool(offsets[0]), + createdAt: reader.readDateTime(offsets[1]), + endDate: reader.readDateTimeOrNull(offsets[2]), + lastModifiedAssetTimestamp: reader.readDateTimeOrNull(offsets[3]), + localId: reader.readStringOrNull(offsets[4]), + modifiedAt: reader.readDateTime(offsets[5]), + name: reader.readString(offsets[6]), + remoteId: reader.readStringOrNull(offsets[7]), + shared: reader.readBool(offsets[8]), + startDate: reader.readDateTimeOrNull(offsets[9]), ); object.id = id; return object; @@ -197,22 +204,24 @@ P _albumDeserializeProp

( ) { switch (propertyId) { case 0: - return (reader.readDateTime(offset)) as P; + return (reader.readBool(offset)) as P; case 1: - return (reader.readDateTimeOrNull(offset)) as P; + return (reader.readDateTime(offset)) as P; case 2: return (reader.readDateTimeOrNull(offset)) as P; case 3: - return (reader.readStringOrNull(offset)) as P; + return (reader.readDateTimeOrNull(offset)) as P; case 4: - return (reader.readDateTime(offset)) as P; - case 5: - return (reader.readString(offset)) as P; - case 6: return (reader.readStringOrNull(offset)) as P; + case 5: + return (reader.readDateTime(offset)) as P; + case 6: + return (reader.readString(offset)) as P; case 7: - return (reader.readBool(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 8: + return (reader.readBool(offset)) as P; + case 9: return (reader.readDateTimeOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -442,6 +451,16 @@ extension AlbumQueryWhere on QueryBuilder { } extension AlbumQueryFilter on QueryBuilder { + QueryBuilder activityEnabledEqualTo( + bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'activityEnabled', + value: value, + )); + }); + } + QueryBuilder createdAtEqualTo( DateTime value) { return QueryBuilder.apply(this, (query) { @@ -1385,6 +1404,18 @@ extension AlbumQueryLinks on QueryBuilder { } extension AlbumQuerySortBy on QueryBuilder { + QueryBuilder sortByActivityEnabled() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'activityEnabled', Sort.asc); + }); + } + + QueryBuilder sortByActivityEnabledDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'activityEnabled', Sort.desc); + }); + } + QueryBuilder sortByCreatedAt() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'createdAt', Sort.asc); @@ -1496,6 +1527,18 @@ extension AlbumQuerySortBy on QueryBuilder { } extension AlbumQuerySortThenBy on QueryBuilder { + QueryBuilder thenByActivityEnabled() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'activityEnabled', Sort.asc); + }); + } + + QueryBuilder thenByActivityEnabledDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'activityEnabled', Sort.desc); + }); + } + QueryBuilder thenByCreatedAt() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'createdAt', Sort.asc); @@ -1619,6 +1662,12 @@ extension AlbumQuerySortThenBy on QueryBuilder { } extension AlbumQueryWhereDistinct on QueryBuilder { + QueryBuilder distinctByActivityEnabled() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'activityEnabled'); + }); + } + QueryBuilder distinctByCreatedAt() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'createdAt'); @@ -1684,6 +1733,12 @@ extension AlbumQueryProperty on QueryBuilder { }); } + QueryBuilder activityEnabledProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'activityEnabled'); + }); + } + QueryBuilder createdAtProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'createdAt'); diff --git a/mobile/lib/shared/models/asset.dart b/mobile/lib/shared/models/asset.dart index 0fcf15ace..74341f7bf 100644 --- a/mobile/lib/shared/models/asset.dart +++ b/mobile/lib/shared/models/asset.dart @@ -6,7 +6,7 @@ import 'package:immich_mobile/utils/hash.dart'; import 'package:isar/isar.dart'; import 'package:openapi/api.dart'; import 'package:photo_manager/photo_manager.dart'; -import 'package:immich_mobile/utils/builtin_extensions.dart'; +import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:path/path.dart' as p; part 'asset.g.dart'; @@ -432,12 +432,12 @@ class Asset { "type": "$type", "fileName": "$fileName", "isFavorite": $isFavorite, - "isRemote: $isRemote, + "isRemote": $isRemote, "storage": "$storage", "width": ${width ?? "N/A"}, "height": ${height ?? "N/A"}, "isArchived": $isArchived, - "isTrashed": $isTrashed, + "isTrashed": $isTrashed }"""; } } diff --git a/mobile/lib/shared/models/server_info/server_config.model.dart b/mobile/lib/shared/models/server_info/server_config.model.dart index 227625dcb..cdb99987e 100644 --- a/mobile/lib/shared/models/server_info/server_config.model.dart +++ b/mobile/lib/shared/models/server_info/server_config.model.dart @@ -2,43 +2,35 @@ import 'package:openapi/api.dart'; class ServerConfig { final int trashDays; - final String mapTileUrl; const ServerConfig({ required this.trashDays, - required this.mapTileUrl, }); ServerConfig copyWith({ int? trashDays, - String? mapTileUrl, }) { return ServerConfig( trashDays: trashDays ?? this.trashDays, - mapTileUrl: mapTileUrl ?? this.mapTileUrl, ); } @override String toString() { - return 'ServerConfig(trashDays: $trashDays, mapTileUrl: $mapTileUrl)'; + return 'ServerConfig(trashDays: $trashDays)'; } - ServerConfig.fromDto(ServerConfigDto dto) - : trashDays = dto.trashDays, - mapTileUrl = dto.mapTileUrl; + ServerConfig.fromDto(ServerConfigDto dto) : trashDays = dto.trashDays; @override bool operator ==(Object other) { if (identical(this, other)) return true; - return other is ServerConfig && - other.trashDays == trashDays && - other.mapTileUrl == mapTileUrl; + return other is ServerConfig && other.trashDays == trashDays; } @override int get hashCode { - return trashDays.hashCode ^ mapTileUrl.hashCode; + return trashDays.hashCode; } } diff --git a/mobile/lib/shared/models/server_info/server_info.model.dart b/mobile/lib/shared/models/server_info/server_info.model.dart index db8150768..b423f598c 100644 --- a/mobile/lib/shared/models/server_info/server_info.model.dart +++ b/mobile/lib/shared/models/server_info/server_info.model.dart @@ -5,43 +5,52 @@ import 'package:immich_mobile/shared/models/server_info/server_version.model.dar class ServerInfo { final ServerVersion serverVersion; + final ServerVersion latestVersion; final ServerFeatures serverFeatures; final ServerConfig serverConfig; final ServerDiskInfo serverDiskInfo; final bool isVersionMismatch; + final bool isNewReleaseAvailable; final String versionMismatchErrorMessage; ServerInfo({ required this.serverVersion, + required this.latestVersion, required this.serverFeatures, required this.serverConfig, - required this.isVersionMismatch, required this.serverDiskInfo, + required this.isVersionMismatch, + required this.isNewReleaseAvailable, required this.versionMismatchErrorMessage, }); ServerInfo copyWith({ ServerVersion? serverVersion, + ServerVersion? latestVersion, ServerFeatures? serverFeatures, ServerConfig? serverConfig, ServerDiskInfo? serverDiskInfo, bool? isVersionMismatch, + bool? isNewReleaseAvailable, String? versionMismatchErrorMessage, }) { return ServerInfo( serverVersion: serverVersion ?? this.serverVersion, + latestVersion: latestVersion ?? this.latestVersion, serverFeatures: serverFeatures ?? this.serverFeatures, serverConfig: serverConfig ?? this.serverConfig, + serverDiskInfo: serverDiskInfo ?? this.serverDiskInfo, isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch, + isNewReleaseAvailable: + isNewReleaseAvailable ?? this.isNewReleaseAvailable, versionMismatchErrorMessage: versionMismatchErrorMessage ?? this.versionMismatchErrorMessage, - serverDiskInfo: serverDiskInfo ?? this.serverDiskInfo, ); } @override String toString() { - return 'ServerInfo(serverVersion: $serverVersion, serverFeatures: $serverFeatures, serverConfig: $serverConfig, isVersionMismatch: $isVersionMismatch, versionMismatchErrorMessage: $versionMismatchErrorMessage, serverDiskInfo: $serverDiskInfo)'; + return 'ServerInfo(serverVersion: $serverVersion, latestVersion: $latestVersion, serverFeatures: $serverFeatures, serverConfig: $serverConfig, serverDiskInfo: $serverDiskInfo, isVersionMismatch: $isVersionMismatch, isNewReleaseAvailable: $isNewReleaseAvailable, versionMismatchErrorMessage: $versionMismatchErrorMessage)'; } @override @@ -50,20 +59,24 @@ class ServerInfo { return other is ServerInfo && other.serverVersion == serverVersion && + other.latestVersion == latestVersion && other.serverFeatures == serverFeatures && other.serverConfig == serverConfig && other.serverDiskInfo == serverDiskInfo && other.isVersionMismatch == isVersionMismatch && + other.isNewReleaseAvailable == isNewReleaseAvailable && other.versionMismatchErrorMessage == versionMismatchErrorMessage; } @override int get hashCode { return serverVersion.hashCode ^ + latestVersion.hashCode ^ serverFeatures.hashCode ^ serverConfig.hashCode ^ + serverDiskInfo.hashCode ^ isVersionMismatch.hashCode ^ - versionMismatchErrorMessage.hashCode ^ - serverDiskInfo.hashCode; + isNewReleaseAvailable.hashCode ^ + versionMismatchErrorMessage.hashCode; } } diff --git a/mobile/lib/shared/models/store.dart b/mobile/lib/shared/models/store.dart index 40258f304..8a186af9a 100644 --- a/mobile/lib/shared/models/store.dart +++ b/mobile/lib/shared/models/store.dart @@ -44,6 +44,7 @@ class Store { /// Stores the value synchronously in the cache and asynchronously in the DB static Future put(StoreKey key, T value) { + if (_cache[key.id] == value) return Future.value(); _cache[key.id] = value; return _db.writeTxn( () async => _db.storeValues.put(await StoreValue._of(value, key)), @@ -52,6 +53,7 @@ class Store { /// Removes the value synchronously from the cache and asynchronously from the DB static Future delete(StoreKey key) { + if (_cache[key.id] == null) return Future.value(); _cache[key.id] = null; return _db.writeTxn(() => _db.storeValues.delete(key.id)); } @@ -151,7 +153,6 @@ enum StoreKey { backupRequireWifi(6, type: bool), backupRequireCharging(7, type: bool), backupTriggerDelay(8, type: int), - githubReleaseInfo(9, type: String), serverUrl(10, type: String), accessToken(11, type: String), serverEndpoint(12, type: String), diff --git a/mobile/lib/shared/models/user.dart b/mobile/lib/shared/models/user.dart index df742a154..094509c89 100644 --- a/mobile/lib/shared/models/user.dart +++ b/mobile/lib/shared/models/user.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/utils/hash.dart'; import 'package:isar/isar.dart'; @@ -11,40 +13,58 @@ class User { required this.id, required this.updatedAt, required this.email, - required this.firstName, - required this.lastName, + required this.name, required this.isAdmin, this.isPartnerSharedBy = false, this.isPartnerSharedWith = false, this.profileImagePath = '', + this.avatarColor = AvatarColorEnum.primary, this.memoryEnabled = true, + this.inTimeline = false, }); Id get isarId => fastHash(id); - User.fromDto(UserResponseDto dto) + User.fromUserDto(UserResponseDto dto) : id = dto.id, updatedAt = dto.updatedAt, email = dto.email, - firstName = dto.firstName, - lastName = dto.lastName, + name = dto.name, isPartnerSharedBy = false, isPartnerSharedWith = false, profileImagePath = dto.profileImagePath, isAdmin = dto.isAdmin, - memoryEnabled = dto.memoriesEnabled; + memoryEnabled = dto.memoriesEnabled ?? false, + avatarColor = dto.avatarColor.toAvatarColor(), + inTimeline = false; + + User.fromPartnerDto(PartnerResponseDto dto) + : id = dto.id, + updatedAt = dto.updatedAt, + email = dto.email, + name = dto.name, + isPartnerSharedBy = false, + isPartnerSharedWith = false, + profileImagePath = dto.profileImagePath, + isAdmin = dto.isAdmin, + memoryEnabled = dto.memoriesEnabled ?? false, + avatarColor = dto.avatarColor.toAvatarColor(), + inTimeline = dto.inTimeline ?? false; @Index(unique: true, replace: false, type: IndexType.hash) String id; DateTime updatedAt; String email; - String firstName; - String lastName; + String name; bool isPartnerSharedBy; bool isPartnerSharedWith; bool isAdmin; String profileImagePath; - bool? memoryEnabled; + @Enumerated(EnumType.ordinal) + AvatarColorEnum avatarColor; + bool memoryEnabled; + bool inTimeline; + @Backlink(to: 'owner') final IsarLinks albums = IsarLinks(); @Backlink(to: 'sharedUsers') @@ -55,14 +75,15 @@ class User { if (other is! User) return false; return id == other.id && updatedAt.isAtSameMomentAs(other.updatedAt) && + avatarColor == other.avatarColor && email == other.email && - firstName == other.firstName && - lastName == other.lastName && + name == other.name && isPartnerSharedBy == other.isPartnerSharedBy && isPartnerSharedWith == other.isPartnerSharedWith && profileImagePath == other.profileImagePath && isAdmin == other.isAdmin && - memoryEnabled == other.memoryEnabled; + memoryEnabled == other.memoryEnabled && + inTimeline == other.inTimeline; } @override @@ -71,11 +92,81 @@ class User { id.hashCode ^ updatedAt.hashCode ^ email.hashCode ^ - firstName.hashCode ^ - lastName.hashCode ^ + name.hashCode ^ isPartnerSharedBy.hashCode ^ isPartnerSharedWith.hashCode ^ profileImagePath.hashCode ^ + avatarColor.hashCode ^ isAdmin.hashCode ^ - memoryEnabled.hashCode; + memoryEnabled.hashCode ^ + inTimeline.hashCode; +} + +enum AvatarColorEnum { + // do not change this order or reuse indices for other purposes, adding is OK + primary, + pink, + red, + yellow, + blue, + green, + purple, + orange, + gray, + amber, +} + +extension AvatarColorEnumHelper on UserAvatarColor { + AvatarColorEnum toAvatarColor() { + switch (this) { + case UserAvatarColor.primary: + return AvatarColorEnum.primary; + case UserAvatarColor.pink: + return AvatarColorEnum.pink; + case UserAvatarColor.red: + return AvatarColorEnum.red; + case UserAvatarColor.yellow: + return AvatarColorEnum.yellow; + case UserAvatarColor.blue: + return AvatarColorEnum.blue; + case UserAvatarColor.green: + return AvatarColorEnum.green; + case UserAvatarColor.purple: + return AvatarColorEnum.purple; + case UserAvatarColor.orange: + return AvatarColorEnum.orange; + case UserAvatarColor.gray: + return AvatarColorEnum.gray; + case UserAvatarColor.amber: + return AvatarColorEnum.amber; + } + return AvatarColorEnum.primary; + } +} + +extension AvatarColorToColorHelper on AvatarColorEnum { + Color toColor([bool isDarkTheme = false]) { + switch (this) { + case AvatarColorEnum.primary: + return isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF); + case AvatarColorEnum.pink: + return const Color.fromARGB(255, 244, 114, 182); + case AvatarColorEnum.red: + return const Color.fromARGB(255, 239, 68, 68); + case AvatarColorEnum.yellow: + return const Color.fromARGB(255, 234, 179, 8); + case AvatarColorEnum.blue: + return const Color.fromARGB(255, 59, 130, 246); + case AvatarColorEnum.green: + return const Color.fromARGB(255, 22, 163, 74); + case AvatarColorEnum.purple: + return const Color.fromARGB(255, 147, 51, 234); + case AvatarColorEnum.orange: + return const Color.fromARGB(255, 234, 88, 12); + case AvatarColorEnum.gray: + return const Color.fromARGB(255, 75, 85, 99); + case AvatarColorEnum.amber: + return const Color.fromARGB(255, 217, 119, 6); + } + } } diff --git a/mobile/lib/shared/models/user.g.dart b/mobile/lib/shared/models/user.g.dart index 687a784c0..0b2605b94 100644 --- a/mobile/lib/shared/models/user.g.dart +++ b/mobile/lib/shared/models/user.g.dart @@ -17,14 +17,15 @@ const UserSchema = CollectionSchema( name: r'User', id: -7838171048429979076, properties: { - r'email': PropertySchema( + r'avatarColor': PropertySchema( id: 0, - name: r'email', - type: IsarType.string, + name: r'avatarColor', + type: IsarType.byte, + enumMap: _UseravatarColorEnumValueMap, ), - r'firstName': PropertySchema( + r'email': PropertySchema( id: 1, - name: r'firstName', + name: r'email', type: IsarType.string, ), r'id': PropertySchema( @@ -32,38 +33,43 @@ const UserSchema = CollectionSchema( name: r'id', type: IsarType.string, ), - r'isAdmin': PropertySchema( + r'inTimeline': PropertySchema( id: 3, + name: r'inTimeline', + type: IsarType.bool, + ), + r'isAdmin': PropertySchema( + id: 4, name: r'isAdmin', type: IsarType.bool, ), r'isPartnerSharedBy': PropertySchema( - id: 4, + id: 5, name: r'isPartnerSharedBy', type: IsarType.bool, ), r'isPartnerSharedWith': PropertySchema( - id: 5, + id: 6, name: r'isPartnerSharedWith', type: IsarType.bool, ), - r'lastName': PropertySchema( - id: 6, - name: r'lastName', - type: IsarType.string, - ), r'memoryEnabled': PropertySchema( id: 7, name: r'memoryEnabled', type: IsarType.bool, ), - r'profileImagePath': PropertySchema( + r'name': PropertySchema( id: 8, + name: r'name', + type: IsarType.string, + ), + r'profileImagePath': PropertySchema( + id: 9, name: r'profileImagePath', type: IsarType.string, ), r'updatedAt': PropertySchema( - id: 9, + id: 10, name: r'updatedAt', type: IsarType.dateTime, ) @@ -118,9 +124,8 @@ int _userEstimateSize( ) { var bytesCount = offsets.last; bytesCount += 3 + object.email.length * 3; - bytesCount += 3 + object.firstName.length * 3; bytesCount += 3 + object.id.length * 3; - bytesCount += 3 + object.lastName.length * 3; + bytesCount += 3 + object.name.length * 3; bytesCount += 3 + object.profileImagePath.length * 3; return bytesCount; } @@ -131,16 +136,17 @@ void _userSerialize( List offsets, Map> allOffsets, ) { - writer.writeString(offsets[0], object.email); - writer.writeString(offsets[1], object.firstName); + writer.writeByte(offsets[0], object.avatarColor.index); + writer.writeString(offsets[1], object.email); writer.writeString(offsets[2], object.id); - writer.writeBool(offsets[3], object.isAdmin); - writer.writeBool(offsets[4], object.isPartnerSharedBy); - writer.writeBool(offsets[5], object.isPartnerSharedWith); - writer.writeString(offsets[6], object.lastName); + writer.writeBool(offsets[3], object.inTimeline); + writer.writeBool(offsets[4], object.isAdmin); + writer.writeBool(offsets[5], object.isPartnerSharedBy); + writer.writeBool(offsets[6], object.isPartnerSharedWith); writer.writeBool(offsets[7], object.memoryEnabled); - writer.writeString(offsets[8], object.profileImagePath); - writer.writeDateTime(offsets[9], object.updatedAt); + writer.writeString(offsets[8], object.name); + writer.writeString(offsets[9], object.profileImagePath); + writer.writeDateTime(offsets[10], object.updatedAt); } User _userDeserialize( @@ -150,16 +156,19 @@ User _userDeserialize( Map> allOffsets, ) { final object = User( - email: reader.readString(offsets[0]), - firstName: reader.readString(offsets[1]), + avatarColor: + _UseravatarColorValueEnumMap[reader.readByteOrNull(offsets[0])] ?? + AvatarColorEnum.primary, + email: reader.readString(offsets[1]), id: reader.readString(offsets[2]), - isAdmin: reader.readBool(offsets[3]), - isPartnerSharedBy: reader.readBoolOrNull(offsets[4]) ?? false, - isPartnerSharedWith: reader.readBoolOrNull(offsets[5]) ?? false, - lastName: reader.readString(offsets[6]), - memoryEnabled: reader.readBoolOrNull(offsets[7]), - profileImagePath: reader.readStringOrNull(offsets[8]) ?? '', - updatedAt: reader.readDateTime(offsets[9]), + inTimeline: reader.readBoolOrNull(offsets[3]) ?? false, + isAdmin: reader.readBool(offsets[4]), + isPartnerSharedBy: reader.readBoolOrNull(offsets[5]) ?? false, + isPartnerSharedWith: reader.readBoolOrNull(offsets[6]) ?? false, + memoryEnabled: reader.readBoolOrNull(offsets[7]) ?? true, + name: reader.readString(offsets[8]), + profileImagePath: reader.readStringOrNull(offsets[9]) ?? '', + updatedAt: reader.readDateTime(offsets[10]), ); return object; } @@ -172,30 +181,58 @@ P _userDeserializeProp

( ) { switch (propertyId) { case 0: - return (reader.readString(offset)) as P; + return (_UseravatarColorValueEnumMap[reader.readByteOrNull(offset)] ?? + AvatarColorEnum.primary) as P; case 1: return (reader.readString(offset)) as P; case 2: return (reader.readString(offset)) as P; case 3: - return (reader.readBool(offset)) as P; - case 4: return (reader.readBoolOrNull(offset) ?? false) as P; + case 4: + return (reader.readBool(offset)) as P; case 5: return (reader.readBoolOrNull(offset) ?? false) as P; case 6: - return (reader.readString(offset)) as P; + return (reader.readBoolOrNull(offset) ?? false) as P; case 7: - return (reader.readBoolOrNull(offset)) as P; + return (reader.readBoolOrNull(offset) ?? true) as P; case 8: - return (reader.readStringOrNull(offset) ?? '') as P; + return (reader.readString(offset)) as P; case 9: + return (reader.readStringOrNull(offset) ?? '') as P; + case 10: return (reader.readDateTime(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } } +const _UseravatarColorEnumValueMap = { + 'primary': 0, + 'pink': 1, + 'red': 2, + 'yellow': 3, + 'blue': 4, + 'green': 5, + 'purple': 6, + 'orange': 7, + 'gray': 8, + 'amber': 9, +}; +const _UseravatarColorValueEnumMap = { + 0: AvatarColorEnum.primary, + 1: AvatarColorEnum.pink, + 2: AvatarColorEnum.red, + 3: AvatarColorEnum.yellow, + 4: AvatarColorEnum.blue, + 5: AvatarColorEnum.green, + 6: AvatarColorEnum.purple, + 7: AvatarColorEnum.orange, + 8: AvatarColorEnum.gray, + 9: AvatarColorEnum.amber, +}; + Id _userGetId(User object) { return object.isarId; } @@ -383,6 +420,59 @@ extension UserQueryWhere on QueryBuilder { } extension UserQueryFilter on QueryBuilder { + QueryBuilder avatarColorEqualTo( + AvatarColorEnum value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'avatarColor', + value: value, + )); + }); + } + + QueryBuilder avatarColorGreaterThan( + AvatarColorEnum value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'avatarColor', + value: value, + )); + }); + } + + QueryBuilder avatarColorLessThan( + AvatarColorEnum value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'avatarColor', + value: value, + )); + }); + } + + QueryBuilder avatarColorBetween( + AvatarColorEnum lower, + AvatarColorEnum upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'avatarColor', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder emailEqualTo( String value, { bool caseSensitive = true, @@ -511,136 +601,6 @@ extension UserQueryFilter on QueryBuilder { }); } - QueryBuilder firstNameEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'firstName', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder firstNameGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'firstName', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder firstNameLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'firstName', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder firstNameBetween( - String lower, - String upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'firstName', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder firstNameStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'firstName', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder firstNameEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'firstName', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder firstNameContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'firstName', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder firstNameMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'firstName', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder firstNameIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'firstName', - value: '', - )); - }); - } - - QueryBuilder firstNameIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'firstName', - value: '', - )); - }); - } - QueryBuilder idEqualTo( String value, { bool caseSensitive = true, @@ -769,6 +729,16 @@ extension UserQueryFilter on QueryBuilder { }); } + QueryBuilder inTimelineEqualTo( + bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'inTimeline', + value: value, + )); + }); + } + QueryBuilder isAdminEqualTo(bool value) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( @@ -850,20 +820,30 @@ extension UserQueryFilter on QueryBuilder { }); } - QueryBuilder lastNameEqualTo( + QueryBuilder memoryEnabledEqualTo( + bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'memoryEnabled', + value: value, + )); + }); + } + + QueryBuilder nameEqualTo( String value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( - property: r'lastName', + property: r'name', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder lastNameGreaterThan( + QueryBuilder nameGreaterThan( String value, { bool include = false, bool caseSensitive = true, @@ -871,14 +851,14 @@ extension UserQueryFilter on QueryBuilder { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( include: include, - property: r'lastName', + property: r'name', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder lastNameLessThan( + QueryBuilder nameLessThan( String value, { bool include = false, bool caseSensitive = true, @@ -886,14 +866,14 @@ extension UserQueryFilter on QueryBuilder { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.lessThan( include: include, - property: r'lastName', + property: r'name', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder lastNameBetween( + QueryBuilder nameBetween( String lower, String upper, { bool includeLower = true, @@ -902,7 +882,7 @@ extension UserQueryFilter on QueryBuilder { }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.between( - property: r'lastName', + property: r'name', lower: lower, includeLower: includeLower, upper: upper, @@ -912,99 +892,72 @@ extension UserQueryFilter on QueryBuilder { }); } - QueryBuilder lastNameStartsWith( + QueryBuilder nameStartsWith( String value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.startsWith( - property: r'lastName', + property: r'name', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder lastNameEndsWith( + QueryBuilder nameEndsWith( String value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.endsWith( - property: r'lastName', + property: r'name', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder lastNameContains(String value, + QueryBuilder nameContains(String value, {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.contains( - property: r'lastName', + property: r'name', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder lastNameMatches( - String pattern, + QueryBuilder nameMatches(String pattern, {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.matches( - property: r'lastName', + property: r'name', wildcard: pattern, caseSensitive: caseSensitive, )); }); } - QueryBuilder lastNameIsEmpty() { + QueryBuilder nameIsEmpty() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( - property: r'lastName', + property: r'name', value: '', )); }); } - QueryBuilder lastNameIsNotEmpty() { + QueryBuilder nameIsNotEmpty() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( - property: r'lastName', + property: r'name', value: '', )); }); } - QueryBuilder memoryEnabledIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'memoryEnabled', - )); - }); - } - - QueryBuilder memoryEnabledIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'memoryEnabled', - )); - }); - } - - QueryBuilder memoryEnabledEqualTo( - bool? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'memoryEnabled', - value: value, - )); - }); - } - QueryBuilder profileImagePathEqualTo( String value, { bool caseSensitive = true, @@ -1305,6 +1258,18 @@ extension UserQueryLinks on QueryBuilder { } extension UserQuerySortBy on QueryBuilder { + QueryBuilder sortByAvatarColor() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'avatarColor', Sort.asc); + }); + } + + QueryBuilder sortByAvatarColorDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'avatarColor', Sort.desc); + }); + } + QueryBuilder sortByEmail() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'email', Sort.asc); @@ -1317,18 +1282,6 @@ extension UserQuerySortBy on QueryBuilder { }); } - QueryBuilder sortByFirstName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'firstName', Sort.asc); - }); - } - - QueryBuilder sortByFirstNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'firstName', Sort.desc); - }); - } - QueryBuilder sortById() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.asc); @@ -1341,6 +1294,18 @@ extension UserQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByInTimeline() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inTimeline', Sort.asc); + }); + } + + QueryBuilder sortByInTimelineDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inTimeline', Sort.desc); + }); + } + QueryBuilder sortByIsAdmin() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'isAdmin', Sort.asc); @@ -1377,18 +1342,6 @@ extension UserQuerySortBy on QueryBuilder { }); } - QueryBuilder sortByLastName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastName', Sort.asc); - }); - } - - QueryBuilder sortByLastNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastName', Sort.desc); - }); - } - QueryBuilder sortByMemoryEnabled() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'memoryEnabled', Sort.asc); @@ -1401,6 +1354,18 @@ extension UserQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder sortByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } + QueryBuilder sortByProfileImagePath() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'profileImagePath', Sort.asc); @@ -1427,6 +1392,18 @@ extension UserQuerySortBy on QueryBuilder { } extension UserQuerySortThenBy on QueryBuilder { + QueryBuilder thenByAvatarColor() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'avatarColor', Sort.asc); + }); + } + + QueryBuilder thenByAvatarColorDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'avatarColor', Sort.desc); + }); + } + QueryBuilder thenByEmail() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'email', Sort.asc); @@ -1439,18 +1416,6 @@ extension UserQuerySortThenBy on QueryBuilder { }); } - QueryBuilder thenByFirstName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'firstName', Sort.asc); - }); - } - - QueryBuilder thenByFirstNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'firstName', Sort.desc); - }); - } - QueryBuilder thenById() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.asc); @@ -1463,6 +1428,18 @@ extension UserQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByInTimeline() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inTimeline', Sort.asc); + }); + } + + QueryBuilder thenByInTimelineDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inTimeline', Sort.desc); + }); + } + QueryBuilder thenByIsAdmin() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'isAdmin', Sort.asc); @@ -1511,18 +1488,6 @@ extension UserQuerySortThenBy on QueryBuilder { }); } - QueryBuilder thenByLastName() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastName', Sort.asc); - }); - } - - QueryBuilder thenByLastNameDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'lastName', Sort.desc); - }); - } - QueryBuilder thenByMemoryEnabled() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'memoryEnabled', Sort.asc); @@ -1535,6 +1500,18 @@ extension UserQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder thenByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } + QueryBuilder thenByProfileImagePath() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'profileImagePath', Sort.asc); @@ -1561,6 +1538,12 @@ extension UserQuerySortThenBy on QueryBuilder { } extension UserQueryWhereDistinct on QueryBuilder { + QueryBuilder distinctByAvatarColor() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'avatarColor'); + }); + } + QueryBuilder distinctByEmail( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -1568,13 +1551,6 @@ extension UserQueryWhereDistinct on QueryBuilder { }); } - QueryBuilder distinctByFirstName( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'firstName', caseSensitive: caseSensitive); - }); - } - QueryBuilder distinctById( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -1582,6 +1558,12 @@ extension UserQueryWhereDistinct on QueryBuilder { }); } + QueryBuilder distinctByInTimeline() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'inTimeline'); + }); + } + QueryBuilder distinctByIsAdmin() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'isAdmin'); @@ -1600,19 +1582,19 @@ extension UserQueryWhereDistinct on QueryBuilder { }); } - QueryBuilder distinctByLastName( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'lastName', caseSensitive: caseSensitive); - }); - } - QueryBuilder distinctByMemoryEnabled() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'memoryEnabled'); }); } + QueryBuilder distinctByName( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'name', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByProfileImagePath( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -1635,24 +1617,30 @@ extension UserQueryProperty on QueryBuilder { }); } + QueryBuilder avatarColorProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'avatarColor'); + }); + } + QueryBuilder emailProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'email'); }); } - QueryBuilder firstNameProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'firstName'); - }); - } - QueryBuilder idProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'id'); }); } + QueryBuilder inTimelineProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'inTimeline'); + }); + } + QueryBuilder isAdminProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'isAdmin'); @@ -1671,15 +1659,15 @@ extension UserQueryProperty on QueryBuilder { }); } - QueryBuilder lastNameProperty() { + QueryBuilder memoryEnabledProperty() { return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'lastName'); + return query.addPropertyName(r'memoryEnabled'); }); } - QueryBuilder memoryEnabledProperty() { + QueryBuilder nameProperty() { return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'memoryEnabled'); + return query.addPropertyName(r'name'); }); } diff --git a/mobile/lib/shared/providers/api.provider.dart b/mobile/lib/shared/providers/api.provider.dart index 24cf864e0..cc73f02b3 100644 --- a/mobile/lib/shared/providers/api.provider.dart +++ b/mobile/lib/shared/providers/api.provider.dart @@ -1,4 +1,7 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; -final apiServiceProvider = Provider((ref) => ApiService()); +part 'api.provider.g.dart'; + +@Riverpod(keepAlive: true) +ApiService apiService(ApiServiceRef ref) => ApiService(); diff --git a/mobile/lib/shared/providers/api.provider.g.dart b/mobile/lib/shared/providers/api.provider.g.dart new file mode 100644 index 000000000..4bc7e93d1 --- /dev/null +++ b/mobile/lib/shared/providers/api.provider.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'api.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$apiServiceHash() => r'03cbd33147a7058d56175e532ac47e1aa4858c6d'; + +/// See also [apiService]. +@ProviderFor(apiService) +final apiServiceProvider = Provider.internal( + apiService, + name: r'apiServiceProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$apiServiceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef ApiServiceRef = ProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/shared/providers/app_state.provider.dart b/mobile/lib/shared/providers/app_state.provider.dart index e2813d86d..0ded4d79d 100644 --- a/mobile/lib/shared/providers/app_state.provider.dart +++ b/mobile/lib/shared/providers/app_state.provider.dart @@ -11,7 +11,6 @@ import 'package:immich_mobile/modules/memories/providers/memory.provider.dart'; import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/modules/settings/providers/notification_permission.provider.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; -import 'package:immich_mobile/shared/providers/release_info.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/tab.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart'; @@ -28,13 +27,10 @@ enum AppStateEnum { } class AppStateNotiifer extends StateNotifier { - final Ref ref; + final Ref _ref; + bool _wasPaused = false; - AppStateNotiifer(this.ref) : super(AppStateEnum.active); - - void updateAppState(AppStateEnum appState) { - state = appState; - } + AppStateNotiifer(this._ref) : super(AppStateEnum.active); AppStateEnum getAppState() { return state; @@ -43,64 +39,72 @@ class AppStateNotiifer extends StateNotifier { void handleAppResume() { state = AppStateEnum.resumed; - var isAuthenticated = ref.watch(authenticationProvider).isAuthenticated; - final permission = ref.watch(galleryPermissionNotifier); + // no need to resume because app was never really paused + if (!_wasPaused) return; + _wasPaused = false; - // Needs to be logged in and have gallery permissions - if (isAuthenticated && (permission.isGranted || permission.isLimited)) { - ref.read(backupProvider.notifier).resumeBackup(); - ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - ref.read(serverInfoProvider.notifier).getServerVersion(); - switch (ref.read(tabProvider)) { + final isAuthenticated = _ref.read(authenticationProvider).isAuthenticated; + + // Needs to be logged in + if (isAuthenticated) { + final permission = _ref.watch(galleryPermissionNotifier); + if (permission.isGranted || permission.isLimited) { + _ref.read(backupProvider.notifier).resumeBackup(); + _ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); + } + _ref.read(serverInfoProvider.notifier).getServerVersion(); + switch (_ref.read(tabProvider)) { case TabEnum.home: - ref.read(assetProvider.notifier).getAllAsset(); + _ref.read(assetProvider.notifier).getAllAsset(); + _ref.read(assetProvider.notifier).getPartnerAssets(); case TabEnum.search: // nothing to do case TabEnum.sharing: - ref.read(assetProvider.notifier).getPartnerAssets(); - ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); + _ref.read(assetProvider.notifier).getPartnerAssets(); + _ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); case TabEnum.library: - ref.read(albumProvider.notifier).getAllAlbums(); + _ref.read(albumProvider.notifier).getAllAlbums(); } } - ref.watch(websocketProvider.notifier).connect(); + _ref.read(websocketProvider.notifier).connect(); - ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo(); - - ref - .watch(notificationPermissionProvider.notifier) + _ref + .read(notificationPermissionProvider.notifier) .getNotificationPermission(); - ref.watch(galleryPermissionNotifier.notifier).getGalleryPermissionStatus(); + _ref.read(galleryPermissionNotifier.notifier).getGalleryPermissionStatus(); - ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); + _ref.read(iOSBackgroundSettingsProvider.notifier).refresh(); - ref.invalidate(memoryFutureProvider); + _ref.invalidate(memoryFutureProvider); } void handleAppInactivity() { state = AppStateEnum.inactive; - - // Do not handle inactivity if manual upload is in progress - if (ref.watch(backupProvider.notifier).backupProgress != - BackUpProgressEnum.manualInProgress) { - ImmichLogger().flush(); - ref.read(websocketProvider.notifier).disconnect(); - ref.read(backupProvider.notifier).cancelBackup(); - } + // do not stop/clean up anything on inactivity: issued on every orientation change } void handleAppPause() { state = AppStateEnum.paused; + _wasPaused = true; + // Do not cancel backup if manual upload is in progress + if (_ref.read(backupProvider.notifier).backupProgress != + BackUpProgressEnum.manualInProgress) { + _ref.read(backupProvider.notifier).cancelBackup(); + } + _ref.read(websocketProvider.notifier).disconnect(); + ImmichLogger().flush(); } void handleAppDetached() { state = AppStateEnum.detached; - ref.watch(manualUploadProvider.notifier).cancelBackup(); + // no guarantee this is called at all + _ref.read(manualUploadProvider.notifier).cancelBackup(); } void handleAppHidden() { state = AppStateEnum.hidden; + // do not stop/clean up anything on inactivity: issued on every orientation change } } diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart index 1694e1f26..50c601801 100644 --- a/mobile/lib/shared/providers/asset.provider.dart +++ b/mobile/lib/shared/providers/asset.provider.dart @@ -8,12 +8,11 @@ import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/services/asset.service.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; -import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; -import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/services/sync.service.dart'; import 'package:immich_mobile/shared/services/user.service.dart'; import 'package:immich_mobile/utils/db.dart'; +import 'package:immich_mobile/utils/renderlist_generator.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -251,26 +250,23 @@ final assetWatcher = return db.assets.watchObject(asset.id, fireImmediately: true); }); -final assetsProvider = - StreamProvider.family((ref, userId) async* { - if (userId == null) return; - final query = ref - .watch(dbProvider) - .assets - .where() - .ownerIdEqualToAnyChecksum(userId) - .filter() - .isArchivedEqualTo(false) - .isTrashedEqualTo(false) - .stackParentIdIsNull() - .sortByFileCreatedAtDesc(); - final settings = ref.watch(appSettingsServiceProvider); - final groupBy = - GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - yield await RenderList.fromQuery(query, groupBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupBy); - } +final assetsProvider = StreamProvider.family((ref, userId) { + if (userId == null) return const Stream.empty(); + final query = _commonFilterAndSort( + _assets(ref).where().ownerIdEqualToAnyChecksum(userId), + ); + return renderListGenerator(query, ref); +}); + +final multiUserAssetsProvider = + StreamProvider.family>((ref, userIds) { + if (userIds.isEmpty) return const Stream.empty(); + final query = _commonFilterAndSort( + _assets(ref) + .where() + .anyOf(userIds, (q, u) => q.ownerIdEqualToAnyChecksum(u)), + ); + return renderListGenerator(query, ref); }); QueryBuilder? getRemoteAssetQuery(WidgetRef ref) { @@ -289,3 +285,17 @@ QueryBuilder? getRemoteAssetQuery(WidgetRef ref) { .stackParentIdIsNull() .sortByFileCreatedAtDesc(); } + +IsarCollection _assets(StreamProviderRef ref) => + ref.watch(dbProvider).assets; + +QueryBuilder _commonFilterAndSort( + QueryBuilder query, +) { + return query + .filter() + .isArchivedEqualTo(false) + .isTrashedEqualTo(false) + .stackParentIdIsNull() + .sortByFileCreatedAtDesc(); +} diff --git a/mobile/lib/shared/providers/release_info.provider.dart b/mobile/lib/shared/providers/release_info.provider.dart deleted file mode 100644 index ded511c00..000000000 --- a/mobile/lib/shared/providers/release_info.provider.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:http/http.dart'; -import 'package:immich_mobile/shared/models/store.dart'; -import 'package:immich_mobile/shared/views/version_announcement_overlay.dart'; -import 'package:logging/logging.dart'; - -class ReleaseInfoNotifier extends StateNotifier { - ReleaseInfoNotifier() : super(""); - final log = Logger('ReleaseInfoNotifier'); - void checkGithubReleaseInfo() async { - final Client client = Client(); - - try { - final String? localReleaseVersion = - Store.tryGet(StoreKey.githubReleaseInfo); - final res = await client.get( - Uri.parse( - "https://api.github.com/repos/immich-app/immich/releases/latest", - ), - headers: {"Accept": "application/vnd.github.v3+json"}, - ); - - if (res.statusCode == 200) { - final data = jsonDecode(res.body); - String latestTagVersion = data["tag_name"]; - state = latestTagVersion; - - if (localReleaseVersion == null && latestTagVersion.isNotEmpty) { - VersionAnnouncementOverlayController.appLoader.show(); - return; - } - - if (latestTagVersion.isNotEmpty && - localReleaseVersion != latestTagVersion) { - VersionAnnouncementOverlayController.appLoader.show(); - return; - } - } - } catch (e) { - debugPrint("Error gettting latest release version"); - - state = ""; - } - } - - void acknowledgeNewVersion() { - Store.put(StoreKey.githubReleaseInfo, state); - VersionAnnouncementOverlayController.appLoader.hide(); - } -} - -final releaseInfoProvider = StateNotifierProvider( - (ref) => ReleaseInfoNotifier(), -); diff --git a/mobile/lib/shared/providers/server_info.provider.dart b/mobile/lib/shared/providers/server_info.provider.dart index b0bcf89b1..b7389824b 100644 --- a/mobile/lib/shared/providers/server_info.provider.dart +++ b/mobile/lib/shared/providers/server_info.provider.dart @@ -18,12 +18,16 @@ class ServerInfoNotifier extends StateNotifier { minor: 0, patch: 0, ), + latestVersion: const ServerVersion( + major: 0, + minor: 0, + patch: 0, + ), serverFeatures: const ServerFeatures( map: true, trash: true, ), serverConfig: const ServerConfig( - mapTileUrl: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", trashDays: 30, ), serverDiskInfo: const ServerDiskInfo( @@ -33,6 +37,7 @@ class ServerInfoNotifier extends StateNotifier { diskUsagePercentage: 0, ), isVersionMismatch: false, + isNewReleaseAvailable: false, versionMismatchErrorMessage: "", ), ); @@ -56,6 +61,10 @@ class ServerInfoNotifier extends StateNotifier { return; } + await _checkServerVersionMismatch(serverVersion); + } + + _checkServerVersionMismatch(ServerVersion serverVersion) async { state = state.copyWith(serverVersion: serverVersion); var packageInfo = await PackageInfo.fromPlatform(); @@ -65,20 +74,32 @@ class ServerInfoNotifier extends StateNotifier { if (appVersion["major"]! > serverVersion.major) { state = state.copyWith( isVersionMismatch: true, - versionMismatchErrorMessage: - "Server is out of date. Please update to the latest major version.", + versionMismatchErrorMessage: "profile_drawer_server_out_of_date_major".tr(), ); + return; + } + if (appVersion["major"]! < serverVersion.major) { + state = state.copyWith( + isVersionMismatch: true, + versionMismatchErrorMessage: "profile_drawer_client_out_of_date_major".tr(), + ); return; } if (appVersion["minor"]! > serverVersion.minor) { state = state.copyWith( isVersionMismatch: true, - versionMismatchErrorMessage: - "Server is out of date. Consider updating to the latest minor version.", + versionMismatchErrorMessage: "profile_drawer_server_out_of_date_minor".tr(), ); + return; + } + if (appVersion["minor"]! < serverVersion.minor) { + state = state.copyWith( + isVersionMismatch: true, + versionMismatchErrorMessage: "profile_drawer_client_out_of_date_minor".tr(), + ); return; } @@ -88,6 +109,25 @@ class ServerInfoNotifier extends StateNotifier { ); } + handleNewRelease( + ServerVersion serverVersion, + ServerVersion latestVersion, + ) { + // Update local server version + _checkServerVersionMismatch(serverVersion); + + final majorEqual = latestVersion.major == serverVersion.major; + final minorEqual = majorEqual && latestVersion.minor == serverVersion.minor; + final newVersionAvailable = latestVersion.major > serverVersion.major || + (majorEqual && latestVersion.minor > serverVersion.minor) || + (minorEqual && latestVersion.patch > serverVersion.patch); + + state = state.copyWith( + latestVersion: latestVersion, + isNewReleaseAvailable: newVersionAvailable, + ); + } + getServerFeatures() async { final serverFeatures = await _serverInfoService.getServerFeatures(); if (serverFeatures == null) { @@ -121,5 +161,5 @@ class ServerInfoNotifier extends StateNotifier { final serverInfoProvider = StateNotifierProvider((ref) { - return ServerInfoNotifier(ref.watch(serverInfoServiceProvider)); + return ServerInfoNotifier(ref.read(serverInfoServiceProvider)); }); diff --git a/mobile/lib/shared/providers/user.provider.dart b/mobile/lib/shared/providers/user.provider.dart index df8ff328d..61c77b24b 100644 --- a/mobile/lib/shared/providers/user.provider.dart +++ b/mobile/lib/shared/providers/user.provider.dart @@ -3,6 +3,8 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/user.dart'; +import 'package:immich_mobile/shared/providers/db.provider.dart'; +import 'package:isar/isar.dart'; class CurrentUserProvider extends StateNotifier { CurrentUserProvider() : super(null) { @@ -24,3 +26,32 @@ final currentUserProvider = StateNotifierProvider((ref) { return CurrentUserProvider(); }); + +class TimelineUserIdsProvider extends StateNotifier> { + TimelineUserIdsProvider(Isar db, User? currentUser) : super([]) { + final query = db.users + .filter() + .inTimelineEqualTo(true) + .or() + .isarIdEqualTo(currentUser?.isarId ?? Isar.autoIncrement) + .isarIdProperty(); + query.findAll().then((users) => state = users); + streamSub = query.watch().listen((users) => state = users); + } + + late final StreamSubscription> streamSub; + + @override + void dispose() { + streamSub.cancel(); + super.dispose(); + } +} + +final timelineUsersIdsProvider = + StateNotifierProvider>((ref) { + return TimelineUserIdsProvider( + ref.watch(dbProvider), + ref.watch(currentUserProvider), + ); +}); diff --git a/mobile/lib/shared/providers/websocket.provider.dart b/mobile/lib/shared/providers/websocket.provider.dart index 83fcb000f..018f7ea7a 100644 --- a/mobile/lib/shared/providers/websocket.provider.dart +++ b/mobile/lib/shared/providers/websocket.provider.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/models/server_info/server_version.model.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; @@ -63,21 +64,19 @@ class WebsocketState { } class WebsocketNotifier extends StateNotifier { - WebsocketNotifier(this.ref) + WebsocketNotifier(this._ref) : super( WebsocketState(socket: null, isConnected: false, pendingChanges: []), - ) { - debounce = Debounce( - const Duration(milliseconds: 500), - ); - } + ); - final log = Logger('WebsocketNotifier'); - final Ref ref; - late final Debounce debounce; + final _log = Logger('WebsocketNotifier'); + final Ref _ref; + final Debounce _debounce = Debounce(const Duration(milliseconds: 500)); - connect() { - var authenticationState = ref.read(authenticationProvider); + /// Connects websocket to server unless already connected + void connect() { + if (state.isConnected) return; + final authenticationState = _ref.read(authenticationProvider); if (authenticationState.isAuthenticated) { final accessToken = Store.get(StoreKey.accessToken); @@ -118,7 +117,7 @@ class WebsocketNotifier extends StateNotifier { }); socket.on('error', (errorMessage) { - log.severe("Websocket Error - $errorMessage"); + _log.severe("Websocket Error - $errorMessage"); state = WebsocketState( isConnected: false, socket: null, @@ -132,13 +131,14 @@ class WebsocketNotifier extends StateNotifier { socket.on('on_asset_trash', _handleServerUpdates); socket.on('on_asset_restore', _handleServerUpdates); socket.on('on_asset_update', _handleServerUpdates); + socket.on('on_new_release', _handleReleaseUpdates); } catch (e) { debugPrint("[WEBSOCKET] Catch Websocket Error - ${e.toString()}"); } } } - disconnect() { + void disconnect() { debugPrint("Attempting to disconnect from websocket"); var socket = state.socket?.disconnect(); @@ -152,30 +152,30 @@ class WebsocketNotifier extends StateNotifier { } } - stopListenToEvent(String eventName) { + void stopListenToEvent(String eventName) { debugPrint("Stop listening to event $eventName"); state.socket?.off(eventName); } - listenUploadEvent() { + void listenUploadEvent() { debugPrint("Start listening to event on_upload_success"); state.socket?.on('on_upload_success', _handleOnUploadSuccess); } - addPendingChange(PendingAction action, dynamic value) { + void addPendingChange(PendingAction action, dynamic value) { state = state.copyWith( pendingChanges: [...state.pendingChanges, PendingChange(action, value)], ); } - handlePendingChanges() { + void handlePendingChanges() { final deleteChanges = state.pendingChanges .where((c) => c.action == PendingAction.assetDelete) .toList(); if (deleteChanges.isNotEmpty) { List remoteIds = deleteChanges.map((a) => a.value.toString()).toList(); - ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); + _ref.read(syncServiceProvider).handleRemoteAssetRemoval(remoteIds); state = state.copyWith( pendingChanges: state.pendingChanges .where((c) => c.action != PendingAction.assetDelete) @@ -184,27 +184,57 @@ class WebsocketNotifier extends StateNotifier { } } - _handleOnUploadSuccess(dynamic data) { + void _handleOnUploadSuccess(dynamic data) { final dto = AssetResponseDto.fromJson(data); if (dto != null) { final newAsset = Asset.remote(dto); - ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset); + _ref.watch(assetProvider.notifier).onNewAssetUploaded(newAsset); } } - _handleOnConfigUpdate(dynamic _) { - ref.read(serverInfoProvider.notifier).getServerFeatures(); - ref.read(serverInfoProvider.notifier).getServerConfig(); + void _handleOnConfigUpdate(dynamic _) { + _ref.read(serverInfoProvider.notifier).getServerFeatures(); + _ref.read(serverInfoProvider.notifier).getServerConfig(); } // Refresh updated assets - _handleServerUpdates(dynamic _) { - ref.read(assetProvider.notifier).getAllAsset(); + void _handleServerUpdates(dynamic _) { + _ref.read(assetProvider.notifier).getAllAsset(); } - _handleOnAssetDelete(dynamic data) { + void _handleOnAssetDelete(dynamic data) { addPendingChange(PendingAction.assetDelete, data); - debounce(handlePendingChanges); + _debounce(handlePendingChanges); + } + + _handleReleaseUpdates(dynamic data) { + // Json guard + if (data is! Map) { + return; + } + + final json = data.cast(); + final serverVersionJson = + json.containsKey('serverVersion') ? json['serverVersion'] : null; + final releaseVersionJson = + json.containsKey('releaseVersion') ? json['releaseVersion'] : null; + if (serverVersionJson == null || releaseVersionJson == null) { + return; + } + + final serverVersionDto = + ServerVersionResponseDto.fromJson(serverVersionJson); + final releaseVersionDto = + ServerVersionResponseDto.fromJson(releaseVersionJson); + if (serverVersionDto == null || releaseVersionDto == null) { + return; + } + + final serverVersion = ServerVersion.fromDto(serverVersionDto); + final releaseVersion = ServerVersion.fromDto(releaseVersionDto); + _ref + .read(serverInfoProvider.notifier) + .handleNewRelease(serverVersion, releaseVersion); } } diff --git a/mobile/lib/shared/services/api.service.dart b/mobile/lib/shared/services/api.service.dart index 2f422515d..656de826c 100644 --- a/mobile/lib/shared/services/api.service.dart +++ b/mobile/lib/shared/services/api.service.dart @@ -22,6 +22,7 @@ class ApiService { late PersonApi personApi; late AuditApi auditApi; late SharedLinkApi sharedLinkApi; + late SystemConfigApi systemConfigApi; late ActivityApi activityApi; ApiService() { @@ -48,6 +49,7 @@ class ApiService { personApi = PersonApi(_apiClient); auditApi = AuditApi(_apiClient); sharedLinkApi = SharedLinkApi(_apiClient); + systemConfigApi = SystemConfigApi(_apiClient); activityApi = ActivityApi(_apiClient); } diff --git a/mobile/lib/shared/services/hash.service.dart b/mobile/lib/shared/services/hash.service.dart index ee272cf5f..914c8bc28 100644 --- a/mobile/lib/shared/services/hash.service.dart +++ b/mobile/lib/shared/services/hash.service.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/device_asset.dart'; import 'package:immich_mobile/shared/models/ios_device_asset.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; -import 'package:immich_mobile/utils/builtin_extensions.dart'; +import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; diff --git a/mobile/lib/shared/services/sync.service.dart b/mobile/lib/shared/services/sync.service.dart index 19fc076e4..08025ed71 100644 --- a/mobile/lib/shared/services/sync.service.dart +++ b/mobile/lib/shared/services/sync.service.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/services/hash.service.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; -import 'package:immich_mobile/utils/builtin_extensions.dart'; +import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; @@ -34,36 +34,8 @@ class SyncService { /// Syncs users from the server to the local database /// Returns `true`if there were any changes - Future syncUsersFromServer(List users) async { - users.sortBy((u) => u.id); - final dbUsers = await _db.users.where().sortById().findAll(); - assert(dbUsers.isSortedBy((u) => u.id), "dbUsers not sorted!"); - final List toDelete = []; - final List toUpsert = []; - final changes = diffSortedListsSync( - users, - dbUsers, - compare: (User a, User b) => a.id.compareTo(b.id), - both: (User a, User b) { - if (!a.updatedAt.isAtSameMomentAs(b.updatedAt) || - a.isPartnerSharedBy != b.isPartnerSharedBy || - a.isPartnerSharedWith != b.isPartnerSharedWith) { - toUpsert.add(a); - return true; - } - return false; - }, - onlyFirst: (User a) => toUpsert.add(a), - onlySecond: (User b) => toDelete.add(b.isarId), - ); - if (changes) { - await _db.writeTxn(() async { - await _db.users.deleteAll(toDelete); - await _db.users.putAll(toUpsert); - }); - } - return changes; - } + Future syncUsersFromServer(List users) => + _lock.run(() => _syncUsersFromServer(users)); /// Syncs remote assets owned by the logged-in user to the DB /// Returns `true` if there were any changes @@ -118,8 +90,44 @@ class SyncService { Future syncNewAssetToDb(Asset newAsset) => _lock.run(() => _syncNewAssetToDb(newAsset)); + Future removeAllLocalAlbumsAndAssets() => + _lock.run(_removeAllLocalAlbumsAndAssets); + // private methods: + /// Syncs users from the server to the local database + /// Returns `true`if there were any changes + Future _syncUsersFromServer(List users) async { + users.sortBy((u) => u.id); + final dbUsers = await _db.users.where().sortById().findAll(); + assert(dbUsers.isSortedBy((u) => u.id), "dbUsers not sorted!"); + final List toDelete = []; + final List toUpsert = []; + final changes = diffSortedListsSync( + users, + dbUsers, + compare: (User a, User b) => a.id.compareTo(b.id), + both: (User a, User b) { + if (!a.updatedAt.isAtSameMomentAs(b.updatedAt) || + a.isPartnerSharedBy != b.isPartnerSharedBy || + a.isPartnerSharedWith != b.isPartnerSharedWith) { + toUpsert.add(a); + return true; + } + return false; + }, + onlyFirst: (User a) => toUpsert.add(a), + onlySecond: (User b) => toDelete.add(b.isarId), + ); + if (changes) { + await _db.writeTxn(() async { + await _db.users.deleteAll(toDelete); + await _db.users.putAll(toUpsert); + }); + } + return changes; + } + /// Syncs a new asset to the db. Returns `true` if successful Future _syncNewAssetToDb(Asset a) async { final Asset? inDb = @@ -751,6 +759,23 @@ class SyncService { await a.assetCountAsync != (await _db.eTags.getById(a.eTagKeyAssetCount))?.assetCount; } + + Future _removeAllLocalAlbumsAndAssets() async { + try { + final assets = await _db.assets.where().localIdIsNotNull().findAll(); + final (toDelete, toUpdate) = + _handleAssetRemoval(assets, [], remote: false); + await _db.writeTxn(() async { + await _db.assets.deleteAll(toDelete); + await _db.assets.putAll(toUpdate); + await _db.albums.where().localIdIsNotNull().deleteAll(); + }); + return true; + } catch (e) { + _log.severe("Failed to remove all local albums and assets: $e"); + return false; + } + } } /// Returns a triple(toAdd, toUpdate, toRemove) @@ -760,15 +785,20 @@ class SyncService { bool? remote, int Function(Asset, Asset) compare = Asset.compareByChecksum, }) { + // fast paths for trivial cases: reduces memory usage during initial sync etc. + if (assets.isEmpty && inDb.isEmpty) { + return const ([], [], []); + } else if (assets.isEmpty && remote == null) { + // remove all from database + return (const [], const [], inDb); + } else if (inDb.isEmpty) { + // add all assets + return (assets, const [], const []); + } + final List toAdd = []; final List toUpdate = []; final List toRemove = []; - if (assets.isEmpty || inDb.isEmpty) { - // fast path for trivial cases: halfes memory usage during initial sync - return assets.isEmpty - ? (toAdd, toUpdate, inDb) // remove all from DB - : (assets, toUpdate, toRemove); // add all assets - } diffSortedListsSync( inDb, assets, diff --git a/mobile/lib/shared/services/user.service.dart b/mobile/lib/shared/services/user.service.dart index 51e3cbc25..4d398c3a8 100644 --- a/mobile/lib/shared/services/user.service.dart +++ b/mobile/lib/shared/services/user.service.dart @@ -40,7 +40,7 @@ class UserService { Future?> _getAllUsers({required bool isAll}) async { try { final dto = await _apiService.userApi.getAllUsers(isAll); - return dto?.map(User.fromDto).toList(); + return dto?.map(User.fromUserDto).toList(); } catch (e) { _log.warning("Failed get all users:\n$e"); return null; @@ -99,7 +99,11 @@ class UserService { users, sharedWith, compare: (User a, User b) => a.id.compareTo(b.id), - both: (User a, User b) => a.isPartnerSharedWith = true, + both: (User a, User b) { + a.isPartnerSharedWith = true; + a.inTimeline = b.inTimeline; + return true; + }, onlyFirst: (_) {}, onlySecond: (_) {}, ); diff --git a/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart index ede113837..053770a92 100644 --- a/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart @@ -1,8 +1,8 @@ -import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/backup/models/backup_state.model.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart'; @@ -22,9 +22,8 @@ class ImmichAppBarDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { BackUpState backupState = ref.watch(backupProvider); - final theme = Theme.of(context); - bool isDarkTheme = theme.brightness == Brightness.dark; - bool isHorizontal = MediaQuery.of(context).size.width > 600; + final theme = context.themeData; + bool isHorizontal = !context.isMobile; final horizontalPadding = isHorizontal ? 100.0 : 20.0; final user = ref.watch(currentUserProvider); @@ -40,7 +39,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { return Row( children: [ InkWell( - onTap: () => Navigator.of(context).pop(), + onTap: () => context.pop(), child: const Icon( Icons.close, size: 20, @@ -54,8 +53,8 @@ class ImmichAppBarDialog extends HookConsumerWidget { style: TextStyle( fontFamily: 'SnowburstOne', fontWeight: FontWeight.bold, - color: Theme.of(context).primaryColor, - fontSize: 15, + color: context.primaryColor, + fontSize: 16, ), ), ), @@ -73,14 +72,15 @@ class ImmichAppBarDialog extends HookConsumerWidget { leading: SizedBox( child: Icon( icon, - color: theme.textTheme.labelMedium?.color, + color: theme.textTheme.labelLarge?.color?.withAlpha(250), size: 20, ), ), title: Text( text, - style: - theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.bold), + style: theme.textTheme.labelLarge?.copyWith( + color: theme.textTheme.labelLarge?.color?.withAlpha(250), + ), ).tr(), onTap: onTap, ); @@ -90,7 +90,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { return buildActionButton( Icons.settings_rounded, "profile_drawer_settings", - () => AutoRouter.of(context).push(const SettingsRoute()), + () => context.autoPush(const SettingsRoute()), ); } @@ -98,7 +98,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { return buildActionButton( Icons.assignment_outlined, "profile_drawer_app_logs", - () => AutoRouter.of(context).push(const AppLogRoute()), + () => context.autoPush(const AppLogRoute()), ); } @@ -121,7 +121,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ref.watch(backupProvider.notifier).cancelBackup(); ref.watch(assetProvider.notifier).clearAllAsset(); ref.watch(websocketProvider.notifier).disconnect(); - AutoRouter.of(context).replace(const LoginRoute()); + context.autoReplace(const LoginRoute()); }, ); }, @@ -136,8 +136,8 @@ class ImmichAppBarDialog extends HookConsumerWidget { child: Container( padding: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( - color: isDarkTheme - ? Theme.of(context).scaffoldBackgroundColor + color: context.isDarkTheme + ? context.scaffoldBackgroundColor : const Color.fromARGB(255, 225, 229, 240), ), child: ListTile( @@ -146,9 +146,11 @@ class ImmichAppBarDialog extends HookConsumerWidget { Icons.storage_rounded, color: theme.primaryColor, ), - title: const Text( + title: Text( "backup_controller_page_server_storage", - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), + style: context.textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w500, + ), ).tr(), isThreeLine: true, subtitle: Padding( @@ -191,7 +193,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { children: [ InkWell( onTap: () { - Navigator.of(context).pop(); + context.pop(); launchUrl( Uri.parse('https://immich.app'), mode: LaunchMode.externalApplication, @@ -199,7 +201,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { }, child: Text( "profile_drawer_documentation", - style: Theme.of(context).textTheme.bodySmall, + style: context.textTheme.bodySmall, ).tr(), ), const SizedBox( @@ -211,7 +213,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ), InkWell( onTap: () { - Navigator.of(context).pop(); + context.pop(); launchUrl( Uri.parse('https://github.com/immich-app/immich'), mode: LaunchMode.externalApplication, @@ -219,7 +221,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { }, child: Text( "profile_drawer_github", - style: Theme.of(context).textTheme.bodySmall, + style: context.textTheme.bodySmall, ).tr(), ), ], @@ -231,7 +233,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { clipBehavior: Clip.hardEdge, alignment: Alignment.topCenter, insetPadding: EdgeInsets.only( - top: isHorizontal ? 20 : 60, + top: isHorizontal ? 20 : 40, left: horizontalPadding, right: horizontalPadding, bottom: isHorizontal ? 20 : 100, diff --git a/mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart index d58699d5c..baae732b1 100644 --- a/mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/shared/ui/app_bar_dialog/app_bar_profile_info.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; @@ -18,38 +19,23 @@ class AppBarProfileInfoBox extends HookConsumerWidget { AuthenticationState authState = ref.watch(authenticationProvider); final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status; - final isDarkMode = Theme.of(context).brightness == Brightness.dark; final user = Store.tryGet(StoreKey.currentUser); buildUserProfileImage() { - const immichImage = CircleAvatar( - radius: 20, - backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), - backgroundColor: Colors.transparent, - ); - - if (authState.profileImagePath.isEmpty || user == null) { - return immichImage; + if (user == null) { + return const CircleAvatar( + radius: 20, + backgroundImage: AssetImage('assets/immich-logo-no-outline.png'), + backgroundColor: Colors.transparent, + ); } final userImage = UserCircleAvatar( - radius: 20, - size: 40, + radius: 22, + size: 44, user: user, ); - if (uploadProfileImageStatus == UploadProfileStatus.idle) { - return authState.profileImagePath.isNotEmpty ? userImage : immichImage; - } - - if (uploadProfileImageStatus == UploadProfileStatus.success) { - return userImage; - } - - if (uploadProfileImageStatus == UploadProfileStatus.failure) { - return immichImage; - } - if (uploadProfileImageStatus == UploadProfileStatus.loading) { return const SizedBox( height: 40, @@ -58,7 +44,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { ); } - return immichImage; + return userImage; } pickUserProfileImage() async { @@ -91,8 +77,8 @@ class AppBarProfileInfoBox extends HookConsumerWidget { child: Container( width: double.infinity, decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Theme.of(context).scaffoldBackgroundColor + color: context.isDarkTheme + ? context.scaffoldBackgroundColor : const Color.fromARGB(255, 225, 229, 240), borderRadius: const BorderRadius.only( topLeft: Radius.circular(10), @@ -111,7 +97,9 @@ class AppBarProfileInfoBox extends HookConsumerWidget { bottom: -5, right: -8, child: Material( - color: isDarkMode ? Colors.blueGrey[800] : Colors.white, + color: context.isDarkTheme + ? Colors.blueGrey[800] + : Colors.white, elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50.0), @@ -120,7 +108,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { padding: const EdgeInsets.all(5.0), child: Icon( Icons.camera_alt_outlined, - color: Theme.of(context).primaryColor, + color: context.primaryColor, size: 14, ), ), @@ -130,18 +118,17 @@ class AppBarProfileInfoBox extends HookConsumerWidget { ), ), title: Text( - "${authState.firstName} ${authState.lastName}", - style: TextStyle( - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.bold, - fontSize: 16, + authState.name, + style: context.textTheme.titleMedium?.copyWith( + color: context.primaryColor, + fontWeight: FontWeight.w500, ), ), subtitle: Text( authState.userEmail, - style: Theme.of(context).textTheme.labelMedium?.copyWith( - fontSize: 12, - ), + style: context.textTheme.bodySmall?.copyWith( + color: context.textTheme.bodySmall?.color?.withAlpha(200), + ), ), ), ), diff --git a/mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart index fa4a73536..40b43838e 100644 --- a/mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/shared/ui/app_bar_dialog/app_bar_server_info.dart @@ -1,6 +1,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/shared/models/server_info/server_info.model.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; @@ -17,6 +18,8 @@ class AppBarServerInfo extends HookConsumerWidget { ServerInfo serverInfoState = ref.watch(serverInfoProvider); final appInfo = useState({}); + const titleFontSize = 12.0; + const contentFontSize = 11.0; getPackageInfo() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); @@ -39,8 +42,8 @@ class AppBarServerInfo extends HookConsumerWidget { padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0), child: Container( decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Theme.of(context).scaffoldBackgroundColor + color: context.isDarkTheme + ? context.scaffoldBackgroundColor : const Color.fromARGB(255, 225, 229, 240), borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(10), @@ -61,8 +64,8 @@ class AppBarServerInfo extends HookConsumerWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 11, - color: Theme.of(context).primaryColor, - fontWeight: FontWeight.w600, + color: context.primaryColor, + fontWeight: FontWeight.w500, ), ), ), @@ -82,9 +85,9 @@ class AppBarServerInfo extends HookConsumerWidget { child: Text( "server_info_box_app_version".tr(), style: TextStyle( - fontSize: 11, - color: Theme.of(context).textTheme.labelSmall?.color, - fontWeight: FontWeight.bold, + fontSize: titleFontSize, + color: context.textTheme.labelSmall?.color, + fontWeight: FontWeight.w500, ), ), ), @@ -96,11 +99,8 @@ class AppBarServerInfo extends HookConsumerWidget { child: Text( "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}", style: TextStyle( - fontSize: 11, - color: Theme.of(context) - .textTheme - .labelSmall - ?.color + fontSize: contentFontSize, + color: context.textTheme.labelSmall?.color ?.withOpacity(0.5), fontWeight: FontWeight.bold, ), @@ -125,9 +125,9 @@ class AppBarServerInfo extends HookConsumerWidget { child: Text( "server_info_box_server_version".tr(), style: TextStyle( - fontSize: 11, - color: Theme.of(context).textTheme.labelSmall?.color, - fontWeight: FontWeight.bold, + fontSize: titleFontSize, + color: context.textTheme.labelSmall?.color, + fontWeight: FontWeight.w500, ), ), ), @@ -139,13 +139,10 @@ class AppBarServerInfo extends HookConsumerWidget { child: Text( serverInfoState.serverVersion.major > 0 ? "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}" - : "?", + : "--", style: TextStyle( - fontSize: 11, - color: Theme.of(context) - .textTheme - .labelSmall - ?.color + fontSize: contentFontSize, + color: context.textTheme.labelSmall?.color ?.withOpacity(0.5), fontWeight: FontWeight.bold, ), @@ -170,9 +167,9 @@ class AppBarServerInfo extends HookConsumerWidget { child: Text( "server_info_box_server_url".tr(), style: TextStyle( - fontSize: 11, - color: Theme.of(context).textTheme.labelSmall?.color, - fontWeight: FontWeight.bold, + fontSize: titleFontSize, + color: context.textTheme.labelSmall?.color, + fontWeight: FontWeight.w500, ), ), ), @@ -185,14 +182,12 @@ class AppBarServerInfo extends HookConsumerWidget { child: Tooltip( verticalOffset: 0, decoration: BoxDecoration( - color: - Theme.of(context).primaryColor.withOpacity(0.9), + color: context.primaryColor.withOpacity(0.9), borderRadius: BorderRadius.circular(10), ), textStyle: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.black - : Colors.white, + color: + context.isDarkTheme ? Colors.black : Colors.white, fontWeight: FontWeight.bold, ), message: getServerUrl() ?? '--', @@ -201,11 +196,8 @@ class AppBarServerInfo extends HookConsumerWidget { child: Text( getServerUrl() ?? '--', style: TextStyle( - fontSize: 11, - color: Theme.of(context) - .textTheme - .labelSmall - ?.color + fontSize: contentFontSize, + color: context.textTheme.labelSmall?.color ?.withOpacity(0.5), fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis, @@ -217,6 +209,61 @@ class AppBarServerInfo extends HookConsumerWidget { ), ], ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Divider( + color: Color.fromARGB(101, 201, 201, 201), + thickness: 1, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Row( + children: [ + if (serverInfoState.isNewReleaseAvailable) + const Padding( + padding: EdgeInsets.only(right: 5.0), + child: Icon( + Icons.info, + color: Color.fromARGB(255, 243, 188, 106), + size: 12, + ), + ), + Text( + "server_info_box_latest_release".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.textTheme.labelSmall?.color + ?.withOpacity(0.5), + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), ], ), ), diff --git a/mobile/lib/shared/ui/confirm_dialog.dart b/mobile/lib/shared/ui/confirm_dialog.dart index 773007f73..4dd3cabbf 100644 --- a/mobile/lib/shared/ui/confirm_dialog.dart +++ b/mobile/lib/shared/ui/confirm_dialog.dart @@ -1,6 +1,7 @@ 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'; class ConfirmDialog extends ConsumerWidget { final Function onOk; @@ -26,11 +27,11 @@ class ConfirmDialog extends ConsumerWidget { content: Text(content).tr(), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(false), + onPressed: () => context.pop(false), child: Text( cancel, style: TextStyle( - color: Theme.of(context).primaryColor, + color: context.primaryColor, fontWeight: FontWeight.bold, ), ).tr(), @@ -38,7 +39,7 @@ class ConfirmDialog extends ConsumerWidget { TextButton( onPressed: () { onOk(); - Navigator.of(context).pop(true); + context.pop(true); }, child: Text( ok, diff --git a/mobile/lib/shared/ui/drag_sheet.dart b/mobile/lib/shared/ui/drag_sheet.dart index 574962cc0..31ed8f482 100644 --- a/mobile/lib/shared/ui/drag_sheet.dart +++ b/mobile/lib/shared/ui/drag_sheet.dart @@ -34,12 +34,13 @@ class ControlBoxButton extends StatelessWidget { padding: const EdgeInsets.all(10), shape: const CircleBorder(), onPressed: onPressed, + minWidth: 75.0, child: Column( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(iconData, size: 24), - const SizedBox(height: 6), + const SizedBox(height: 4), Text( label, style: const TextStyle(fontSize: 12.0), diff --git a/mobile/lib/shared/ui/immich_app_bar.dart b/mobile/lib/shared/ui/immich_app_bar.dart index 3510931f6..49f5453a7 100644 --- a/mobile/lib/shared/ui/immich_app_bar.dart +++ b/mobile/lib/shared/ui/immich_app_bar.dart @@ -1,11 +1,9 @@ -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/shared/models/store.dart'; import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_dialog.dart'; import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; -import 'package:immich_mobile/modules/login/models/authentication_state.model.dart'; -import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/modules/backup/models/backup_state.model.dart'; @@ -26,9 +24,8 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { final bool isEnableAutoBackup = backupState.backgroundBackup || backupState.autoBackup; final ServerInfo serverInfoState = ref.watch(serverInfoProvider); - AuthenticationState authState = ref.watch(authenticationProvider); final user = Store.tryGet(StoreKey.currentUser); - final isDarkMode = Theme.of(context).brightness == Brightness.dark; + final isDarkTheme = context.isDarkTheme; const widgetSize = 30.0; buildProfileIndicator() { @@ -53,9 +50,11 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { ), backgroundColor: Colors.transparent, alignment: Alignment.bottomRight, - isLabelVisible: serverInfoState.isVersionMismatch, + isLabelVisible: serverInfoState.isVersionMismatch || + ((user?.isAdmin ?? false) && + serverInfoState.isNewReleaseAvailable), offset: const Offset(2, 2), - child: authState.profileImagePath.isEmpty || user == null + child: user == null ? const Icon( Icons.face_outlined, size: widgetSize, @@ -70,7 +69,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { } getBackupBadgeIcon() { - final iconColor = isDarkMode ? Colors.white : Colors.black; + final iconColor = isDarkTheme ? Colors.white : Colors.black; if (isEnableAutoBackup) { if (backupState.backupProgress == BackUpProgressEnum.inProgress) { @@ -104,10 +103,10 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { buildBackupIndicator() { final indicatorIcon = getBackupBadgeIcon(); - final badgeBackground = isDarkMode ? Colors.blueGrey[800] : Colors.white; + final badgeBackground = isDarkTheme ? Colors.blueGrey[800] : Colors.white; return InkWell( - onTap: () => AutoRouter.of(context).push(const BackupControllerRoute()), + onTap: () => context.autoPush(const BackupControllerRoute()), borderRadius: BorderRadius.circular(12), child: Badge( label: Container( @@ -116,7 +115,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { decoration: BoxDecoration( color: badgeBackground, border: Border.all( - color: isDarkMode ? Colors.black : Colors.grey, + color: isDarkTheme ? Colors.black : Colors.grey, ), borderRadius: BorderRadius.circular(widgetSize / 2), ), @@ -129,14 +128,14 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { child: Icon( Icons.backup_rounded, size: widgetSize, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), ); } return AppBar( - backgroundColor: Theme.of(context).appBarTheme.backgroundColor, + backgroundColor: context.themeData.appBarTheme.backgroundColor, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(5), diff --git a/mobile/lib/shared/ui/immich_image.dart b/mobile/lib/shared/ui/immich_image.dart index 08a049558..1182bd8c9 100644 --- a/mobile/lib/shared/ui/immich_image.dart +++ b/mobile/lib/shared/ui/immich_image.dart @@ -1,14 +1,15 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_thumbhash/flutter_thumbhash.dart'; import 'package:image_fade/image_fade.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:openapi/api.dart' as api; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; /// Renders an Asset using local data if available, else remote data class ImmichImage extends StatelessWidget { @@ -88,7 +89,7 @@ class ImmichImage extends StatelessWidget { } return Icon( Icons.image_not_supported_outlined, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ); }, ); @@ -137,7 +138,7 @@ class ImmichImage extends StatelessWidget { } return Icon( Icons.image_not_supported_outlined, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ); }, ); diff --git a/mobile/lib/shared/ui/immich_loading_indicator.dart b/mobile/lib/shared/ui/immich_loading_indicator.dart index 98ddb8f47..24eedcd47 100644 --- a/mobile/lib/shared/ui/immich_loading_indicator.dart +++ b/mobile/lib/shared/ui/immich_loading_indicator.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; class ImmichLoadingIndicator extends StatelessWidget { final double? borderRadius; @@ -14,13 +15,13 @@ class ImmichLoadingIndicator extends StatelessWidget { height: 60, width: 60, decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withAlpha(200), + color: context.primaryColor.withAlpha(200), borderRadius: BorderRadius.circular(borderRadius ?? 10), ), padding: const EdgeInsets.all(15), child: const CircularProgressIndicator( color: Colors.white, - strokeWidth: 2, + strokeWidth: 3, ), ); } diff --git a/mobile/lib/shared/ui/immich_title_text.dart b/mobile/lib/shared/ui/immich_title_text.dart index 7f633a0e6..3ef0501dd 100644 --- a/mobile/lib/shared/ui/immich_title_text.dart +++ b/mobile/lib/shared/ui/immich_title_text.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; class ImmichTitleText extends StatelessWidget { final double fontSize; @@ -18,9 +19,8 @@ class ImmichTitleText extends StatelessWidget { fontFamily: 'SnowburstOne', fontWeight: FontWeight.bold, fontSize: fontSize, - color: color ?? Theme.of(context).primaryColor, + color: color ?? context.primaryColor, ), ); } - } diff --git a/mobile/lib/shared/ui/immich_toast.dart b/mobile/lib/shared/ui/immich_toast.dart index 3f15c13a2..25a0e65fa 100644 --- a/mobile/lib/shared/ui/immich_toast.dart +++ b/mobile/lib/shared/ui/immich_toast.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; enum ToastType { info, success, error } @@ -11,14 +12,13 @@ class ImmichToast { ToastGravity gravity = ToastGravity.TOP, int durationInSecond = 3, }) { - final isDarkTheme = Theme.of(context).brightness == Brightness.dark; final fToast = FToast(); fToast.init(context); Color getColor(ToastType type, BuildContext context) { switch (type) { case ToastType.info: - return Theme.of(context).primaryColor; + return context.primaryColor; case ToastType.success: return const Color.fromARGB(255, 78, 140, 124); case ToastType.error: @@ -31,7 +31,7 @@ class ImmichToast { case ToastType.info: return Icon( Icons.info_outline_rounded, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ); case ToastType.success: return const Icon( @@ -51,7 +51,7 @@ class ImmichToast { padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5.0), - color: isDarkTheme ? Colors.grey[900] : Colors.grey[50], + color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[50], border: Border.all( color: Colors.black12, width: 1, diff --git a/mobile/lib/shared/ui/scaffold_error_body.dart b/mobile/lib/shared/ui/scaffold_error_body.dart new file mode 100644 index 000000000..fef6bef59 --- /dev/null +++ b/mobile/lib/shared/ui/scaffold_error_body.dart @@ -0,0 +1,36 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +// Error widget to be used in Scaffold when an AsyncError is received +class ScaffoldErrorBody extends StatelessWidget { + final bool withIcon; + + const ScaffoldErrorBody({super.key, this.withIcon = true}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "scaffold_body_error_occured", + style: context.textTheme.displayMedium, + textAlign: TextAlign.center, + ).tr(), + if (withIcon) + Center( + child: Padding( + padding: const EdgeInsets.only(top: 15), + child: Icon( + Icons.error_outline, + size: 100, + color: context.themeData.iconTheme.color?.withOpacity(0.5), + ), + ), + ), + ], + ); + } +} diff --git a/mobile/lib/shared/ui/user_avatar.dart b/mobile/lib/shared/ui/user_avatar.dart index c736e8f90..95f76de43 100644 --- a/mobile/lib/shared/ui/user_avatar.dart +++ b/mobile/lib/shared/ui/user_avatar.dart @@ -1,16 +1,16 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/user.dart'; Widget userAvatar(BuildContext context, User u, {double? radius}) { final url = "${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${u.id}"; - final firstNameFirstLetter = u.firstName.isNotEmpty ? u.firstName[0] : ""; - final lastNameFirstLetter = u.lastName.isNotEmpty ? u.lastName[0] : ""; + final nameFirstLetter = u.name.isNotEmpty ? u.name[0] : ""; return CircleAvatar( radius: radius, - backgroundColor: Theme.of(context).primaryColor.withAlpha(50), + backgroundColor: context.primaryColor.withAlpha(50), foregroundImage: CachedNetworkImageProvider( url, headers: {"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"}, @@ -18,6 +18,6 @@ Widget userAvatar(BuildContext context, User u, {double? radius}) { ), // silence errors if user has no profile image, use initials as fallback onForegroundImageError: (exception, stackTrace) {}, - child: Text((firstNameFirstLetter + lastNameFirstLetter).toUpperCase()), + child: Text(nameFirstLetter.toUpperCase()), ); } diff --git a/mobile/lib/shared/ui/user_circle_avatar.dart b/mobile/lib/shared/ui/user_circle_avatar.dart index df50d5071..1f6ef15f5 100644 --- a/mobile/lib/shared/ui/user_circle_avatar.dart +++ b/mobile/lib/shared/ui/user_circle_avatar.dart @@ -12,50 +12,34 @@ class UserCircleAvatar extends ConsumerWidget { final User user; double radius; double size; - bool useRandomBackgroundColor; UserCircleAvatar({ super.key, this.radius = 22, this.size = 44, - this.useRandomBackgroundColor = false, required this.user, }); @override Widget build(BuildContext context, WidgetRef ref) { - final randomColors = [ - Colors.red[200], - Colors.blue[200], - Colors.green[200], - Colors.yellow[200], - Colors.purple[200], - Colors.orange[200], - Colors.pink[200], - Colors.teal[200], - Colors.indigo[200], - Colors.cyan[200], - Colors.brown[200], - ]; - + bool isDarkTheme = Theme.of(context).brightness == Brightness.dark; final profileImageUrl = '${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${user.id}?d=${Random().nextInt(1024)}'; final textIcon = Text( - user.firstName[0].toUpperCase(), + user.name[0].toUpperCase(), style: TextStyle( fontWeight: FontWeight.bold, - color: Theme.of(context).brightness == Brightness.dark + fontSize: 12, + color: isDarkTheme && user.avatarColor == AvatarColorEnum.primary ? Colors.black : Colors.white, ), ); return CircleAvatar( - backgroundColor: useRandomBackgroundColor - ? randomColors[Random().nextInt(randomColors.length)] - : Theme.of(context).primaryColor, + backgroundColor: user.avatarColor.toColor(), radius: radius, - child: user.profileImagePath == "" + child: user.profileImagePath.isEmpty ? textIcon : ClipRRect( borderRadius: BorderRadius.circular(50), diff --git a/mobile/lib/shared/views/app_log_detail_page.dart b/mobile/lib/shared/views/app_log_detail_page.dart index 0963605b4..8b737fb5c 100644 --- a/mobile/lib/shared/views/app_log_detail_page.dart +++ b/mobile/lib/shared/views/app_log_detail_page.dart @@ -1,5 +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/shared/models/logger_message.model.dart'; import 'package:flutter/services.dart'; @@ -10,7 +11,7 @@ class AppLogDetailPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - var isDarkMode = Theme.of(context).brightness == Brightness.dark; + var isDarkTheme = context.isDarkTheme; buildStackMessage(String stackTrace) { return Padding( @@ -28,7 +29,7 @@ class AppLogDetailPage extends HookConsumerWidget { "STACK TRACES", style: TextStyle( fontSize: 12.0, - color: Theme.of(context).primaryColor, + color: context.primaryColor, fontWeight: FontWeight.bold, ), ), @@ -38,21 +39,28 @@ class AppLogDetailPage extends HookConsumerWidget { Clipboard.setData(ClipboardData(text: stackTrace)) .then((_) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Copied to clipboard")), + SnackBar( + content: Text( + "Copied to clipboard", + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ), + ), ); }); }, icon: Icon( Icons.copy, size: 16.0, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), ], ), Container( decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.grey[200], + color: isDarkTheme ? Colors.grey[900] : Colors.grey[200], borderRadius: BorderRadius.circular(15.0), ), child: Padding( @@ -88,7 +96,7 @@ class AppLogDetailPage extends HookConsumerWidget { "MESSAGE", style: TextStyle( fontSize: 12.0, - color: Theme.of(context).primaryColor, + color: context.primaryColor, fontWeight: FontWeight.bold, ), ), @@ -97,21 +105,28 @@ class AppLogDetailPage extends HookConsumerWidget { onPressed: () { Clipboard.setData(ClipboardData(text: message)).then((_) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Copied to clipboard")), + SnackBar( + content: Text( + "Copied to clipboard", + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ), + ), ); }); }, icon: Icon( Icons.copy, size: 16.0, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), ], ), Container( decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.grey[200], + color: isDarkTheme ? Colors.grey[900] : Colors.grey[200], borderRadius: BorderRadius.circular(15.0), ), child: Padding( @@ -143,14 +158,14 @@ class AppLogDetailPage extends HookConsumerWidget { "FROM", style: TextStyle( fontSize: 12.0, - color: Theme.of(context).primaryColor, + color: context.primaryColor, fontWeight: FontWeight.bold, ), ), ), Container( decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.grey[200], + color: isDarkTheme ? Colors.grey[900] : Colors.grey[200], borderRadius: BorderRadius.circular(15.0), ), child: Padding( diff --git a/mobile/lib/shared/views/app_log_page.dart b/mobile/lib/shared/views/app_log_page.dart index a8dfc6ec4..9d1dca19f 100644 --- a/mobile/lib/shared/views/app_log_page.dart +++ b/mobile/lib/shared/views/app_log_page.dart @@ -1,7 +1,7 @@ -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/logger_message.model.dart'; import 'package:immich_mobile/shared/services/immich_logger.service.dart'; @@ -16,6 +16,7 @@ class AppLogPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final immichLogger = ImmichLogger(); final logMessages = useState(immichLogger.messages); + final isDarkTheme = context.isDarkTheme; Widget colorStatusIndicator(Color color) { return Column( @@ -36,7 +37,7 @@ class AppLogPage extends HookConsumerWidget { Widget buildLeadingIcon(LogLevel level) { switch (level) { case LogLevel.INFO: - return colorStatusIndicator(Theme.of(context).primaryColor); + return colorStatusIndicator(context.primaryColor); case LogLevel.SEVERE: return colorStatusIndicator(Colors.redAccent); @@ -52,15 +53,15 @@ class AppLogPage extends HookConsumerWidget { case LogLevel.INFO: return Colors.transparent; case LogLevel.SEVERE: - return Theme.of(context).brightness == Brightness.dark + return isDarkTheme ? Colors.redAccent.withOpacity(0.25) : Colors.redAccent.withOpacity(0.075); case LogLevel.WARNING: - return Theme.of(context).brightness == Brightness.dark + return isDarkTheme ? Colors.orangeAccent.withOpacity(0.25) : Colors.orangeAccent.withOpacity(0.075); default: - return Theme.of(context).primaryColor.withOpacity(0.1); + return context.primaryColor.withOpacity(0.1); } } @@ -79,7 +80,7 @@ class AppLogPage extends HookConsumerWidget { IconButton( icon: Icon( Icons.delete_outline_rounded, - color: Theme.of(context).primaryColor, + color: context.primaryColor, semanticLabel: "Clear logs", size: 20.0, ), @@ -91,7 +92,7 @@ class AppLogPage extends HookConsumerWidget { IconButton( icon: Icon( Icons.share_rounded, - color: Theme.of(context).primaryColor, + color: context.primaryColor, semanticLabel: "Share logs", size: 20.0, ), @@ -102,7 +103,7 @@ class AppLogPage extends HookConsumerWidget { ], leading: IconButton( onPressed: () { - AutoRouter.of(context).pop(); + context.autoPop(); }, icon: const Icon( Icons.arrow_back_ios_new_rounded, @@ -115,16 +116,14 @@ class AppLogPage extends HookConsumerWidget { separatorBuilder: (context, index) { return Divider( height: 0, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white70 - : Colors.grey[600], + color: isDarkTheme ? Colors.white70 : Colors.grey[600], ); }, itemCount: logMessages.value.length, itemBuilder: (context, index) { var logMessage = logMessages.value[index]; return ListTile( - onTap: () => AutoRouter.of(context).push( + onTap: () => context.autoPush( AppLogDetailRoute( logMessage: logMessage, ), @@ -140,9 +139,7 @@ class AppLogPage extends HookConsumerWidget { TextSpan( text: "#$index ", style: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white70 - : Colors.grey[600], + color: isDarkTheme ? Colors.white70 : Colors.grey[600], fontSize: 14.0, fontWeight: FontWeight.bold, ), @@ -170,7 +167,7 @@ class AppLogPage extends HookConsumerWidget { ), ); } - + /// Truncate the log message to a certain number of lines /// @param int maxLines - Max number of lines to truncate String truncateLogMessage(String message, int maxLines) { diff --git a/mobile/lib/shared/views/immich_loading_overlay.dart b/mobile/lib/shared/views/immich_loading_overlay.dart index 6e4ef166b..85f0123ed 100644 --- a/mobile/lib/shared/views/immich_loading_overlay.dart +++ b/mobile/lib/shared/views/immich_loading_overlay.dart @@ -1,41 +1,64 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; -class ImmichLoadingOverlay extends StatelessWidget { - const ImmichLoadingOverlay({ - Key? key, - }) : super(key: key); +final _loadingEntry = OverlayEntry( + builder: (context) => SizedBox.square( + dimension: double.infinity, + child: DecoratedBox( + decoration: + BoxDecoration(color: context.colorScheme.surface.withAlpha(200)), + child: const Center(child: ImmichLoadingIndicator()), + ), + ), +); + +ValueNotifier useProcessingOverlay() { + return use(const _LoadingOverlay()); +} + +class _LoadingOverlay extends Hook> { + const _LoadingOverlay(); @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: - ImmichLoadingOverlayController.appLoader.loaderShowingNotifier, - builder: (context, shouldShow, child) { - return shouldShow - ? const Scaffold( - backgroundColor: Colors.black54, - body: Center( - child: ImmichLoadingIndicator(), - ), - ) - : const SizedBox(); - }, - ); - } + _LoadingOverlayState createState() => _LoadingOverlayState(); } -class ImmichLoadingOverlayController { - static final ImmichLoadingOverlayController appLoader = - ImmichLoadingOverlayController(); - ValueNotifier loaderShowingNotifier = ValueNotifier(false); - ValueNotifier loaderTextNotifier = ValueNotifier('error message'); +class _LoadingOverlayState + extends HookState, _LoadingOverlay> { + late final _isProcessing = ValueNotifier(false)..addListener(_listener); + OverlayEntry? overlayEntry; - void show() { - loaderShowingNotifier.value = true; + void _listener() { + setState(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_isProcessing.value) { + overlayEntry?.remove(); + overlayEntry = _loadingEntry; + Overlay.of(context).insert(_loadingEntry); + } else { + overlayEntry?.remove(); + overlayEntry = null; + } + }); + }); } - void hide() { - loaderShowingNotifier.value = false; + @override + ValueNotifier build(BuildContext context) { + return _isProcessing; } + + @override + void dispose() { + _isProcessing.dispose(); + super.dispose(); + } + + @override + Object? get debugValue => _isProcessing.value; + + @override + String get debugLabel => 'useProcessingOverlay<>'; } diff --git a/mobile/lib/shared/views/splash_screen.dart b/mobile/lib/shared/views/splash_screen.dart index bd419bc02..1298e0efe 100644 --- a/mobile/lib/shared/views/splash_screen.dart +++ b/mobile/lib/shared/views/splash_screen.dart @@ -1,7 +1,7 @@ -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart'; @@ -40,18 +40,31 @@ class SplashScreenPage extends HookConsumerWidget { log.severe(e); } - isSuccess = - await ref.read(authenticationProvider.notifier).setSuccessLoginInfo( - accessToken: accessToken, - serverUrl: serverUrl, - offlineLogin: deviceIsOffline, - ); + try { + isSuccess = await ref + .read(authenticationProvider.notifier) + .setSuccessLoginInfo( + accessToken: accessToken, + serverUrl: serverUrl, + offlineLogin: deviceIsOffline, + ); + } catch (error, stackTrace) { + ref.read(authenticationProvider.notifier).logout(); + + log.severe( + 'Cannot set success login info: $error', + error, + stackTrace, + ); + + context.autoPush(const LoginRoute()); + } } // If the device is offline and there is a currentUser stored locallly // Proceed into the app if (deviceIsOffline && Store.tryGet(StoreKey.currentUser) != null) { - AutoRouter.of(context).replace(const TabControllerRoute()); + context.autoReplace(const TabControllerRoute()); } else if (isSuccess) { // If device was able to login through the internet successfully final hasPermission = @@ -60,10 +73,10 @@ class SplashScreenPage extends HookConsumerWidget { // Resume backup (if enable) then navigate ref.watch(backupProvider.notifier).resumeBackup(); } - AutoRouter.of(context).replace(const TabControllerRoute()); + context.autoReplace(const TabControllerRoute()); } else { // User was unable to login through either offline or online methods - AutoRouter.of(context).replace(const LoginRoute()); + context.autoReplace(const LoginRoute()); } } @@ -72,7 +85,7 @@ class SplashScreenPage extends HookConsumerWidget { if (serverUrl != null && accessToken != null) { performLoggingIn(); } else { - AutoRouter.of(context).replace(const LoginRoute()); + context.autoReplace(const LoginRoute()); } return null; }, diff --git a/mobile/lib/shared/views/tab_controller_page.dart b/mobile/lib/shared/views/tab_controller_page.dart index 8e72ce900..2b9d62565 100644 --- a/mobile/lib/shared/views/tab_controller_page.dart +++ b/mobile/lib/shared/views/tab_controller_page.dart @@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart'; import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -31,7 +32,7 @@ class TabControllerPage extends HookConsumerWidget { child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( - Theme.of(context).primaryColor, + context.primaryColor, ), ), ), @@ -55,10 +56,10 @@ class TabControllerPage extends HookConsumerWidget { ref.read(tabProvider.notifier).state = TabEnum.values[index]; }, selectedIconTheme: IconThemeData( - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), selectedLabelTextStyle: TextStyle( - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), useIndicator: false, destinations: [ @@ -116,7 +117,7 @@ class TabControllerPage extends HookConsumerWidget { selectedIcon: buildIcon( Icon( Icons.photo_library, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), ), @@ -127,7 +128,7 @@ class TabControllerPage extends HookConsumerWidget { ), selectedIcon: Icon( Icons.search, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), NavigationDestination( @@ -137,7 +138,7 @@ class TabControllerPage extends HookConsumerWidget { ), selectedIcon: Icon( Icons.group, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), NavigationDestination( @@ -148,7 +149,7 @@ class TabControllerPage extends HookConsumerWidget { selectedIcon: buildIcon( Icon( Icons.photo_album_rounded, - color: Theme.of(context).primaryColor, + color: context.primaryColor, ), ), ), diff --git a/mobile/lib/shared/views/version_announcement_overlay.dart b/mobile/lib/shared/views/version_announcement_overlay.dart deleted file mode 100644 index 7a01ef304..000000000 --- a/mobile/lib/shared/views/version_announcement_overlay.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/shared/providers/release_info.provider.dart'; -import 'package:immich_mobile/shared/providers/admin_provider.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class VersionAnnouncementOverlay extends HookConsumerWidget { - const VersionAnnouncementOverlay({ - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final bool isAdmin = ref.watch(isAdminProvider); - - if (!isAdmin) { - return const SizedBox.shrink(); // Don't show anything for non-admins - } - - void goToReleaseNote() async { - final Uri url = - Uri.parse('https://github.com/immich-app/immich/releases/latest'); - await launchUrl(url); - } - - void onAcknowledgeTapped() { - ref.watch(releaseInfoProvider.notifier).acknowledgeNewVersion(); - } - - return ValueListenableBuilder( - valueListenable: - VersionAnnouncementOverlayController.appLoader.loaderShowingNotifier, - builder: (context, shouldShow, child) { - if (shouldShow) { - return Scaffold( - backgroundColor: Colors.black38, - body: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 307), - child: Wrap( - children: [ - Card( - child: Padding( - padding: const EdgeInsets.all(30.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "version_announcement_overlay_title", - style: TextStyle( - fontSize: 16, - fontFamily: 'WorkSans', - fontWeight: FontWeight.bold, - color: Colors.indigo, - ), - ).tr(), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: RichText( - text: TextSpan( - style: const TextStyle( - fontSize: 14, - fontFamily: 'WorkSans', - color: Colors.black87, - height: 1.2, - ), - children: [ - TextSpan( - text: - 'version_announcement_overlay_text_1' - .tr(), - ), - const TextSpan( - text: ' Immich ', - style: TextStyle( - fontFamily: "SnowBurstOne", - color: Colors.indigo, - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: - "version_announcement_overlay_text_2" - .tr(), - ), - TextSpan( - text: - "version_announcement_overlay_release_notes" - .tr(), - style: const TextStyle( - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = goToReleaseNote, - ), - TextSpan( - text: - "version_announcement_overlay_text_3" - .tr(), - ), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - shape: const StadiumBorder(), - visualDensity: VisualDensity.standard, - backgroundColor: Colors.indigo, - foregroundColor: Colors.grey[50], - elevation: 2, - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 25, - ), - ), - onPressed: onAcknowledgeTapped, - child: const Text( - "version_announcement_overlay_ack", - style: TextStyle( - fontSize: 14, - ), - ).tr(), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } else { - return const SizedBox(); - } - }, - ); - } -} - -class VersionAnnouncementOverlayController { - static final VersionAnnouncementOverlayController appLoader = - VersionAnnouncementOverlayController(); - ValueNotifier loaderShowingNotifier = ValueNotifier(false); - ValueNotifier loaderTextNotifier = ValueNotifier('error message'); - - void show() { - loaderShowingNotifier.value = true; - } - - void hide() { - loaderShowingNotifier.value = false; - } -} diff --git a/mobile/lib/utils/capitalize.dart b/mobile/lib/utils/capitalize.dart deleted file mode 100644 index 80b10cc58..000000000 --- a/mobile/lib/utils/capitalize.dart +++ /dev/null @@ -1,9 +0,0 @@ -extension StringExtension on String { - String capitalize() { - return split(" ") - .map( - (str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1), - ) - .join(" "); - } -} diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/utils/immich_app_theme.dart index 670a7660d..4313da60f 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/utils/immich_app_theme.dart @@ -38,19 +38,19 @@ ThemeData immichLightTheme = ThemeData( hintColor: Colors.indigo, focusColor: Colors.indigo, splashColor: Colors.indigo.withOpacity(0.15), - fontFamily: 'WorkSans', + fontFamily: 'Overpass', scaffoldBackgroundColor: immichBackgroundColor, snackBarTheme: const SnackBarThemeData( contentTextStyle: TextStyle( - fontFamily: 'WorkSans', + fontFamily: 'Overpass', color: Colors.indigo, fontWeight: FontWeight.bold, ), backgroundColor: Colors.white, ), - appBarTheme: AppBarTheme( - titleTextStyle: const TextStyle( - fontFamily: 'WorkSans', + appBarTheme: const AppBarTheme( + titleTextStyle: TextStyle( + fontFamily: 'Overpass', color: Colors.indigo, fontWeight: FontWeight.bold, fontSize: 18, @@ -61,7 +61,7 @@ ThemeData immichLightTheme = ThemeData( scrolledUnderElevation: 0, centerTitle: true, ), - bottomNavigationBarTheme: BottomNavigationBarThemeData( + bottomNavigationBarTheme: const BottomNavigationBarThemeData( type: BottomNavigationBarType.fixed, backgroundColor: immichBackgroundColor, selectedItemColor: Colors.indigo, @@ -69,7 +69,7 @@ ThemeData immichLightTheme = ThemeData( cardTheme: const CardTheme( surfaceTintColor: Colors.transparent, ), - drawerTheme: DrawerThemeData( + drawerTheme: const DrawerThemeData( backgroundColor: immichBackgroundColor, ), textTheme: const TextTheme( @@ -125,9 +125,9 @@ ThemeData immichLightTheme = ThemeData( surfaceTintColor: Colors.transparent, labelTextStyle: MaterialStatePropertyAll( TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.grey[700], + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.grey[800], ), ), ), @@ -160,10 +160,10 @@ ThemeData immichDarkTheme = ThemeData( primaryColor: immichDarkThemePrimaryColor, scaffoldBackgroundColor: immichDarkBackgroundColor, hintColor: Colors.grey[600], - fontFamily: 'WorkSans', + fontFamily: 'Overpass', snackBarTheme: SnackBarThemeData( - contentTextStyle: TextStyle( - fontFamily: 'WorkSans', + contentTextStyle: const TextStyle( + fontFamily: 'Overpass', color: immichDarkThemePrimaryColor, fontWeight: FontWeight.bold, ), @@ -174,35 +174,35 @@ ThemeData immichDarkTheme = ThemeData( foregroundColor: immichDarkThemePrimaryColor, ), ), - appBarTheme: AppBarTheme( + appBarTheme: const AppBarTheme( titleTextStyle: TextStyle( - fontFamily: 'WorkSans', + fontFamily: 'Overpass', color: immichDarkThemePrimaryColor, fontWeight: FontWeight.bold, fontSize: 18, ), - backgroundColor: const Color.fromARGB(255, 32, 33, 35), + backgroundColor: Color.fromARGB(255, 32, 33, 35), foregroundColor: immichDarkThemePrimaryColor, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, ), - bottomNavigationBarTheme: BottomNavigationBarThemeData( + bottomNavigationBarTheme: const BottomNavigationBarThemeData( type: BottomNavigationBarType.fixed, - backgroundColor: const Color.fromARGB(255, 35, 36, 37), + backgroundColor: Color.fromARGB(255, 35, 36, 37), selectedItemColor: immichDarkThemePrimaryColor, ), drawerTheme: DrawerThemeData( backgroundColor: immichDarkBackgroundColor, scrimColor: Colors.white.withOpacity(0.1), ), - textTheme: TextTheme( - displayLarge: const TextStyle( + textTheme: const TextTheme( + displayLarge: TextStyle( fontSize: 26, fontWeight: FontWeight.bold, color: Color.fromARGB(255, 255, 255, 255), ), - displayMedium: const TextStyle( + displayMedium: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Color.fromARGB(255, 255, 255, 255), @@ -212,15 +212,15 @@ ThemeData immichDarkTheme = ThemeData( fontWeight: FontWeight.bold, color: immichDarkThemePrimaryColor, ), - titleSmall: const TextStyle( + titleSmall: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, ), - titleMedium: const TextStyle( + titleMedium: TextStyle( fontSize: 18.0, fontWeight: FontWeight.bold, ), - titleLarge: const TextStyle( + titleLarge: TextStyle( fontSize: 26.0, fontWeight: FontWeight.bold, ), @@ -249,16 +249,16 @@ ThemeData immichDarkTheme = ThemeData( surfaceTintColor: Colors.transparent, labelTextStyle: MaterialStatePropertyAll( TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.grey[500], + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.grey[300], ), ), ), dialogTheme: const DialogTheme( surfaceTintColor: Colors.transparent, ), - inputDecorationTheme: InputDecorationTheme( + inputDecorationTheme: const InputDecorationTheme( focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: immichDarkThemePrimaryColor, @@ -267,12 +267,12 @@ ThemeData immichDarkTheme = ThemeData( labelStyle: TextStyle( color: immichDarkThemePrimaryColor, ), - hintStyle: const TextStyle( + hintStyle: TextStyle( fontSize: 14.0, fontWeight: FontWeight.normal, ), ), - textSelectionTheme: TextSelectionThemeData( + textSelectionTheme: const TextSelectionThemeData( cursorColor: immichDarkThemePrimaryColor, ), ); diff --git a/mobile/lib/utils/renderlist_generator.dart b/mobile/lib/utils/renderlist_generator.dart new file mode 100644 index 000000000..c68957a2a --- /dev/null +++ b/mobile/lib/utils/renderlist_generator.dart @@ -0,0 +1,26 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; +import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; +import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:isar/isar.dart'; + +Stream renderListGenerator( + QueryBuilder query, + StreamProviderRef ref, +) { + final settings = ref.watch(appSettingsServiceProvider); + final groupBy = + GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; + return renderListGeneratorWithGroupBy(query, groupBy); +} + +Stream renderListGeneratorWithGroupBy( + QueryBuilder query, + GroupAssetsBy groupBy, +) async* { + yield await RenderList.fromQuery(query, groupBy); + await for (final _ in query.watchLazy()) { + yield await RenderList.fromQuery(query, groupBy); + } +} diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart index 511dcf81e..d52f3ac1d 100644 --- a/mobile/lib/utils/selection_handlers.dart +++ b/mobile/lib/utils/selection_handlers.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/services/share.service.dart'; @@ -26,7 +27,7 @@ void handleShareAssets( gravity: ToastGravity.BOTTOM, ); } - Navigator.of(buildContext).pop(); + buildContext.pop(); }, ); return const ShareDialog(); diff --git a/mobile/lib/utils/url_helper.dart b/mobile/lib/utils/url_helper.dart index b771a6f70..95fcd8078 100644 --- a/mobile/lib/utils/url_helper.dart +++ b/mobile/lib/utils/url_helper.dart @@ -3,10 +3,10 @@ import 'package:immich_mobile/shared/models/store.dart'; String sanitizeUrl(String url) { // Add schema if none is set final urlWithSchema = - url.startsWith(RegExp(r"https?://")) ? url : "https://$url"; + url.trimLeft().startsWith(RegExp(r"https?://")) ? url : "https://$url"; // Remove trailing slash(es) - return urlWithSchema.replaceFirst(RegExp(r"/+$"), ""); + return urlWithSchema.trimRight().replaceFirst(RegExp(r"/+$"), ""); } String? getServerUrl() { diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 52b863a6f..f54b788a4 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -13,7 +13,6 @@ doc/ActivityCreateDto.md doc/ActivityResponseDto.md doc/ActivityStatisticsResponseDto.md doc/AddUsersDto.md -doc/AdminSignupResponseDto.md doc/AlbumApi.md doc/AlbumCountResponseDto.md doc/AlbumResponseDto.md @@ -30,6 +29,7 @@ doc/AssetIdsDto.md doc/AssetIdsResponseDto.md doc/AssetJobName.md doc/AssetJobsDto.md +doc/AssetOrder.md doc/AssetResponseDto.md doc/AssetStatsResponseDto.md doc/AssetTypeEnum.md @@ -46,7 +46,6 @@ doc/CQMode.md doc/ChangePasswordDto.md doc/CheckExistingAssetsDto.md doc/CheckExistingAssetsResponseDto.md -doc/CitiesFile.md doc/ClassificationConfig.md doc/Colorspace.md doc/CreateAlbumDto.md @@ -66,7 +65,6 @@ doc/FileChecksumResponseDto.md doc/FileReportDto.md doc/FileReportFixDto.md doc/FileReportItemDto.md -doc/ImportAssetDto.md doc/JobApi.md doc/JobCommand.md doc/JobCommandDto.md @@ -82,6 +80,7 @@ doc/LoginCredentialDto.md doc/LoginResponseDto.md doc/LogoutResponseDto.md doc/MapMarkerResponseDto.md +doc/MapTheme.md doc/MemoryLaneResponseDto.md doc/MergePersonDto.md doc/ModelType.md @@ -91,6 +90,7 @@ doc/OAuthCallbackDto.md doc/OAuthConfigDto.md doc/OAuthConfigResponseDto.md doc/PartnerApi.md +doc/PartnerResponseDto.md doc/PathEntityType.md doc/PathType.md doc/PeopleResponseDto.md @@ -101,12 +101,12 @@ doc/PersonResponseDto.md doc/PersonStatisticsResponseDto.md doc/PersonUpdateDto.md doc/QueueStatusDto.md +doc/ReactionLevel.md doc/ReactionType.md doc/RecognitionConfig.md doc/ScanLibraryDto.md doc/SearchAlbumResponseDto.md doc/SearchApi.md -doc/SearchAssetDto.md doc/SearchAssetResponseDto.md doc/SearchExploreItem.md doc/SearchExploreResponseDto.md @@ -158,11 +158,13 @@ doc/TranscodePolicy.md doc/UpdateAlbumDto.md doc/UpdateAssetDto.md doc/UpdateLibraryDto.md +doc/UpdatePartnerDto.md doc/UpdateStackParentDto.md doc/UpdateTagDto.md doc/UpdateUserDto.md doc/UsageByUserDto.md doc/UserApi.md +doc/UserAvatarColor.md doc/UserDto.md doc/UserResponseDto.md doc/ValidateAccessTokenResponseDto.md @@ -198,7 +200,6 @@ lib/model/activity_create_dto.dart lib/model/activity_response_dto.dart lib/model/activity_statistics_response_dto.dart lib/model/add_users_dto.dart -lib/model/admin_signup_response_dto.dart lib/model/album_count_response_dto.dart lib/model/album_response_dto.dart lib/model/all_job_status_response_dto.dart @@ -217,6 +218,7 @@ lib/model/asset_ids_dto.dart lib/model/asset_ids_response_dto.dart lib/model/asset_job_name.dart lib/model/asset_jobs_dto.dart +lib/model/asset_order.dart lib/model/asset_response_dto.dart lib/model/asset_stats_response_dto.dart lib/model/asset_type_enum.dart @@ -228,7 +230,6 @@ lib/model/bulk_ids_dto.dart lib/model/change_password_dto.dart lib/model/check_existing_assets_dto.dart lib/model/check_existing_assets_response_dto.dart -lib/model/cities_file.dart lib/model/classification_config.dart lib/model/clip_config.dart lib/model/clip_mode.dart @@ -251,7 +252,6 @@ lib/model/file_checksum_response_dto.dart lib/model/file_report_dto.dart lib/model/file_report_fix_dto.dart lib/model/file_report_item_dto.dart -lib/model/import_asset_dto.dart lib/model/job_command.dart lib/model/job_command_dto.dart lib/model/job_counts_dto.dart @@ -265,6 +265,7 @@ lib/model/login_credential_dto.dart lib/model/login_response_dto.dart lib/model/logout_response_dto.dart lib/model/map_marker_response_dto.dart +lib/model/map_theme.dart lib/model/memory_lane_response_dto.dart lib/model/merge_person_dto.dart lib/model/model_type.dart @@ -272,6 +273,7 @@ lib/model/o_auth_authorize_response_dto.dart lib/model/o_auth_callback_dto.dart lib/model/o_auth_config_dto.dart lib/model/o_auth_config_response_dto.dart +lib/model/partner_response_dto.dart lib/model/path_entity_type.dart lib/model/path_type.dart lib/model/people_response_dto.dart @@ -281,11 +283,11 @@ lib/model/person_response_dto.dart lib/model/person_statistics_response_dto.dart lib/model/person_update_dto.dart lib/model/queue_status_dto.dart +lib/model/reaction_level.dart lib/model/reaction_type.dart lib/model/recognition_config.dart lib/model/scan_library_dto.dart lib/model/search_album_response_dto.dart -lib/model/search_asset_dto.dart lib/model/search_asset_response_dto.dart lib/model/search_explore_item.dart lib/model/search_explore_response_dto.dart @@ -333,10 +335,12 @@ lib/model/transcode_policy.dart lib/model/update_album_dto.dart lib/model/update_asset_dto.dart lib/model/update_library_dto.dart +lib/model/update_partner_dto.dart lib/model/update_stack_parent_dto.dart lib/model/update_tag_dto.dart lib/model/update_user_dto.dart lib/model/usage_by_user_dto.dart +lib/model/user_avatar_color.dart lib/model/user_dto.dart lib/model/user_response_dto.dart lib/model/validate_access_token_response_dto.dart @@ -347,7 +351,6 @@ test/activity_create_dto_test.dart test/activity_response_dto_test.dart test/activity_statistics_response_dto_test.dart test/add_users_dto_test.dart -test/admin_signup_response_dto_test.dart test/album_api_test.dart test/album_count_response_dto_test.dart test/album_response_dto_test.dart @@ -369,6 +372,7 @@ test/asset_ids_dto_test.dart test/asset_ids_response_dto_test.dart test/asset_job_name_test.dart test/asset_jobs_dto_test.dart +test/asset_order_test.dart test/asset_response_dto_test.dart test/asset_stats_response_dto_test.dart test/asset_type_enum_test.dart @@ -382,7 +386,6 @@ test/bulk_ids_dto_test.dart test/change_password_dto_test.dart test/check_existing_assets_dto_test.dart test/check_existing_assets_response_dto_test.dart -test/cities_file_test.dart test/classification_config_test.dart test/clip_config_test.dart test/clip_mode_test.dart @@ -405,7 +408,6 @@ test/file_checksum_response_dto_test.dart test/file_report_dto_test.dart test/file_report_fix_dto_test.dart test/file_report_item_dto_test.dart -test/import_asset_dto_test.dart test/job_api_test.dart test/job_command_dto_test.dart test/job_command_test.dart @@ -421,6 +423,7 @@ test/login_credential_dto_test.dart test/login_response_dto_test.dart test/logout_response_dto_test.dart test/map_marker_response_dto_test.dart +test/map_theme_test.dart test/memory_lane_response_dto_test.dart test/merge_person_dto_test.dart test/model_type_test.dart @@ -430,6 +433,7 @@ test/o_auth_callback_dto_test.dart test/o_auth_config_dto_test.dart test/o_auth_config_response_dto_test.dart test/partner_api_test.dart +test/partner_response_dto_test.dart test/path_entity_type_test.dart test/path_type_test.dart test/people_response_dto_test.dart @@ -440,12 +444,12 @@ test/person_response_dto_test.dart test/person_statistics_response_dto_test.dart test/person_update_dto_test.dart test/queue_status_dto_test.dart +test/reaction_level_test.dart test/reaction_type_test.dart test/recognition_config_test.dart test/scan_library_dto_test.dart test/search_album_response_dto_test.dart test/search_api_test.dart -test/search_asset_dto_test.dart test/search_asset_response_dto_test.dart test/search_explore_item_test.dart test/search_explore_response_dto_test.dart @@ -497,11 +501,13 @@ test/transcode_policy_test.dart test/update_album_dto_test.dart test/update_asset_dto_test.dart test/update_library_dto_test.dart +test/update_partner_dto_test.dart test/update_stack_parent_dto_test.dart test/update_tag_dto_test.dart test/update_user_dto_test.dart test/usage_by_user_dto_test.dart test/user_api_test.dart +test/user_avatar_color_test.dart test/user_dto_test.dart test/user_response_dto_test.dart test/validate_access_token_response_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 70ef217a5..42680e679 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.84.0 +- API version: 1.89.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements @@ -98,6 +98,7 @@ Class | Method | HTTP request | Description *AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **POST** /asset/download/{id} | *AssetApi* | [**emptyTrash**](doc//AssetApi.md#emptytrash) | **POST** /asset/trash/empty | *AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset | +*AssetApi* | [**getAllUserAssetsByDeviceId**](doc//AssetApi.md#getalluserassetsbydeviceid) | **GET** /asset/device/{deviceId} | *AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | *AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms | *AssetApi* | [**getAssetStatistics**](doc//AssetApi.md#getassetstatistics) | **GET** /asset/statistics | @@ -110,12 +111,11 @@ Class | Method | HTTP request | Description *AssetApi* | [**getRandom**](doc//AssetApi.md#getrandom) | **GET** /asset/random | *AssetApi* | [**getTimeBucket**](doc//AssetApi.md#gettimebucket) | **GET** /asset/time-bucket | *AssetApi* | [**getTimeBuckets**](doc//AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | -*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | -*AssetApi* | [**importFile**](doc//AssetApi.md#importfile) | **POST** /asset/import | +*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | Use /asset/device/:deviceId instead - Remove in 1.92 release *AssetApi* | [**restoreAssets**](doc//AssetApi.md#restoreassets) | **POST** /asset/restore | *AssetApi* | [**restoreTrash**](doc//AssetApi.md#restoretrash) | **POST** /asset/trash/restore | *AssetApi* | [**runAssetJobs**](doc//AssetApi.md#runassetjobs) | **POST** /asset/jobs | -*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search | +*AssetApi* | [**searchAssets**](doc//AssetApi.md#searchassets) | **GET** /assets | *AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} | *AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{id} | *AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset | @@ -152,6 +152,7 @@ Class | Method | HTTP request | Description *PartnerApi* | [**createPartner**](doc//PartnerApi.md#createpartner) | **POST** /partner/{id} | *PartnerApi* | [**getPartners**](doc//PartnerApi.md#getpartners) | **GET** /partner | *PartnerApi* | [**removePartner**](doc//PartnerApi.md#removepartner) | **DELETE** /partner/{id} | +*PartnerApi* | [**updatePartner**](doc//PartnerApi.md#updatepartner) | **PUT** /partner/{id} | *PersonApi* | [**getAllPeople**](doc//PersonApi.md#getallpeople) | **GET** /person | *PersonApi* | [**getPerson**](doc//PersonApi.md#getperson) | **GET** /person/{id} | *PersonApi* | [**getPersonAssets**](doc//PersonApi.md#getpersonassets) | **GET** /person/{id}/assets | @@ -181,6 +182,7 @@ Class | Method | HTTP request | Description *SharedLinkApi* | [**updateSharedLink**](doc//SharedLinkApi.md#updatesharedlink) | **PATCH** /shared-link/{id} | *SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config | *SystemConfigApi* | [**getConfigDefaults**](doc//SystemConfigApi.md#getconfigdefaults) | **GET** /system-config/defaults | +*SystemConfigApi* | [**getMapStyle**](doc//SystemConfigApi.md#getmapstyle) | **GET** /system-config/map/style.json | *SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options | *SystemConfigApi* | [**updateConfig**](doc//SystemConfigApi.md#updateconfig) | **PUT** /system-config | *TagApi* | [**createTag**](doc//TagApi.md#createtag) | **POST** /tag | @@ -193,6 +195,7 @@ Class | Method | HTTP request | Description *TagApi* | [**updateTag**](doc//TagApi.md#updatetag) | **PATCH** /tag/{id} | *UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image | *UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user | +*UserApi* | [**deleteProfileImage**](doc//UserApi.md#deleteprofileimage) | **DELETE** /user/profile-image | *UserApi* | [**deleteUser**](doc//UserApi.md#deleteuser) | **DELETE** /user/{id} | *UserApi* | [**getAllUsers**](doc//UserApi.md#getallusers) | **GET** /user | *UserApi* | [**getMyUserInfo**](doc//UserApi.md#getmyuserinfo) | **GET** /user/me | @@ -212,7 +215,6 @@ Class | Method | HTTP request | Description - [ActivityResponseDto](doc//ActivityResponseDto.md) - [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md) - [AddUsersDto](doc//AddUsersDto.md) - - [AdminSignupResponseDto](doc//AdminSignupResponseDto.md) - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md) - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md) @@ -227,6 +229,7 @@ Class | Method | HTTP request | Description - [AssetIdsResponseDto](doc//AssetIdsResponseDto.md) - [AssetJobName](doc//AssetJobName.md) - [AssetJobsDto](doc//AssetJobsDto.md) + - [AssetOrder](doc//AssetOrder.md) - [AssetResponseDto](doc//AssetResponseDto.md) - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetTypeEnum](doc//AssetTypeEnum.md) @@ -241,7 +244,6 @@ Class | Method | HTTP request | Description - [ChangePasswordDto](doc//ChangePasswordDto.md) - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) - - [CitiesFile](doc//CitiesFile.md) - [ClassificationConfig](doc//ClassificationConfig.md) - [Colorspace](doc//Colorspace.md) - [CreateAlbumDto](doc//CreateAlbumDto.md) @@ -261,7 +263,6 @@ Class | Method | HTTP request | Description - [FileReportDto](doc//FileReportDto.md) - [FileReportFixDto](doc//FileReportFixDto.md) - [FileReportItemDto](doc//FileReportItemDto.md) - - [ImportAssetDto](doc//ImportAssetDto.md) - [JobCommand](doc//JobCommand.md) - [JobCommandDto](doc//JobCommandDto.md) - [JobCountsDto](doc//JobCountsDto.md) @@ -275,6 +276,7 @@ Class | Method | HTTP request | Description - [LoginResponseDto](doc//LoginResponseDto.md) - [LogoutResponseDto](doc//LogoutResponseDto.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) + - [MapTheme](doc//MapTheme.md) - [MemoryLaneResponseDto](doc//MemoryLaneResponseDto.md) - [MergePersonDto](doc//MergePersonDto.md) - [ModelType](doc//ModelType.md) @@ -282,6 +284,7 @@ Class | Method | HTTP request | Description - [OAuthCallbackDto](doc//OAuthCallbackDto.md) - [OAuthConfigDto](doc//OAuthConfigDto.md) - [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md) + - [PartnerResponseDto](doc//PartnerResponseDto.md) - [PathEntityType](doc//PathEntityType.md) - [PathType](doc//PathType.md) - [PeopleResponseDto](doc//PeopleResponseDto.md) @@ -291,11 +294,11 @@ Class | Method | HTTP request | Description - [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md) - [PersonUpdateDto](doc//PersonUpdateDto.md) - [QueueStatusDto](doc//QueueStatusDto.md) + - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) - [RecognitionConfig](doc//RecognitionConfig.md) - [ScanLibraryDto](doc//ScanLibraryDto.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) - - [SearchAssetDto](doc//SearchAssetDto.md) - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) - [SearchExploreItem](doc//SearchExploreItem.md) - [SearchExploreResponseDto](doc//SearchExploreResponseDto.md) @@ -343,10 +346,12 @@ Class | Method | HTTP request | Description - [UpdateAlbumDto](doc//UpdateAlbumDto.md) - [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateLibraryDto](doc//UpdateLibraryDto.md) + - [UpdatePartnerDto](doc//UpdatePartnerDto.md) - [UpdateStackParentDto](doc//UpdateStackParentDto.md) - [UpdateTagDto](doc//UpdateTagDto.md) - [UpdateUserDto](doc//UpdateUserDto.md) - [UsageByUserDto](doc//UsageByUserDto.md) + - [UserAvatarColor](doc//UserAvatarColor.md) - [UserDto](doc//UserDto.md) - [UserResponseDto](doc//UserResponseDto.md) - [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md) diff --git a/mobile/openapi/doc/ActivityApi.md b/mobile/openapi/doc/ActivityApi.md index 1af3f1f49..6221ef5e4 100644 --- a/mobile/openapi/doc/ActivityApi.md +++ b/mobile/openapi/doc/ActivityApi.md @@ -125,7 +125,7 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getActivities** -> List getActivities(albumId, assetId, type, userId) +> List getActivities(albumId, assetId, type, level, userId) @@ -151,10 +151,11 @@ final api_instance = ActivityApi(); final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final assetId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final type = ; // ReactionType | +final level = ; // ReactionLevel | final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | try { - final result = api_instance.getActivities(albumId, assetId, type, userId); + final result = api_instance.getActivities(albumId, assetId, type, level, userId); print(result); } catch (e) { print('Exception when calling ActivityApi->getActivities: $e\n'); @@ -168,6 +169,7 @@ Name | Type | Description | Notes **albumId** | **String**| | **assetId** | **String**| | [optional] **type** | [**ReactionType**](.md)| | [optional] + **level** | [**ReactionLevel**](.md)| | [optional] **userId** | **String**| | [optional] ### Return type diff --git a/mobile/openapi/doc/AlbumResponseDto.md b/mobile/openapi/doc/AlbumResponseDto.md index 93620b9fc..bc00d30af 100644 --- a/mobile/openapi/doc/AlbumResponseDto.md +++ b/mobile/openapi/doc/AlbumResponseDto.md @@ -17,6 +17,7 @@ Name | Type | Description | Notes **endDate** | [**DateTime**](DateTime.md) | | [optional] **hasSharedLink** | **bool** | | **id** | **String** | | +**isActivityEnabled** | **bool** | | **lastModifiedAssetTimestamp** | [**DateTime**](DateTime.md) | | [optional] **owner** | [**UserResponseDto**](UserResponseDto.md) | | **ownerId** | **String** | | diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 811841947..b479c08f3 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -16,6 +16,7 @@ Method | HTTP request | Description [**downloadFile**](AssetApi.md#downloadfile) | **POST** /asset/download/{id} | [**emptyTrash**](AssetApi.md#emptytrash) | **POST** /asset/trash/empty | [**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset | +[**getAllUserAssetsByDeviceId**](AssetApi.md#getalluserassetsbydeviceid) | **GET** /asset/device/{deviceId} | [**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | [**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms | [**getAssetStatistics**](AssetApi.md#getassetstatistics) | **GET** /asset/statistics | @@ -28,12 +29,11 @@ Method | HTTP request | Description [**getRandom**](AssetApi.md#getrandom) | **GET** /asset/random | [**getTimeBucket**](AssetApi.md#gettimebucket) | **GET** /asset/time-bucket | [**getTimeBuckets**](AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | -[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | -[**importFile**](AssetApi.md#importfile) | **POST** /asset/import | +[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | Use /asset/device/:deviceId instead - Remove in 1.92 release [**restoreAssets**](AssetApi.md#restoreassets) | **POST** /asset/restore | [**restoreTrash**](AssetApi.md#restoretrash) | **POST** /asset/trash/restore | [**runAssetJobs**](AssetApi.md#runassetjobs) | **POST** /asset/jobs | -[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search | +[**searchAssets**](AssetApi.md#searchassets) | **GET** /assets | [**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} | [**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{id} | [**updateAssets**](AssetApi.md#updateassets) | **PUT** /asset | @@ -444,6 +444,63 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **getAllUserAssetsByDeviceId** +> List getAllUserAssetsByDeviceId(deviceId) + + + +Get all asset of a device that are in the database, ID only. + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = AssetApi(); +final deviceId = deviceId_example; // String | + +try { + final result = api_instance.getAllUserAssetsByDeviceId(deviceId); + print(result); +} catch (e) { + print('Exception when calling AssetApi->getAllUserAssetsByDeviceId: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **deviceId** | **String**| | + +### Return type + +**List** + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **getAssetById** > AssetResponseDto getAssetById(id, key) @@ -1005,7 +1062,7 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getTimeBucket** -> List getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key) +> List getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key) @@ -1037,10 +1094,11 @@ final isArchived = true; // bool | final isFavorite = true; // bool | final isTrashed = true; // bool | final withStacked = true; // bool | +final withPartners = true; // bool | final key = key_example; // String | try { - final result = api_instance.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key); + final result = api_instance.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key); print(result); } catch (e) { print('Exception when calling AssetApi->getTimeBucket: $e\n'); @@ -1060,6 +1118,7 @@ Name | Type | Description | Notes **isFavorite** | **bool**| | [optional] **isTrashed** | **bool**| | [optional] **withStacked** | **bool**| | [optional] + **withPartners** | **bool**| | [optional] **key** | **String**| | [optional] ### Return type @@ -1078,7 +1137,7 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getTimeBuckets** -> List getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key) +> List getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key) @@ -1109,10 +1168,11 @@ final isArchived = true; // bool | final isFavorite = true; // bool | final isTrashed = true; // bool | final withStacked = true; // bool | +final withPartners = true; // bool | final key = key_example; // String | try { - final result = api_instance.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key); + final result = api_instance.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key); print(result); } catch (e) { print('Exception when calling AssetApi->getTimeBuckets: $e\n'); @@ -1131,6 +1191,7 @@ Name | Type | Description | Notes **isFavorite** | **bool**| | [optional] **isTrashed** | **bool**| | [optional] **withStacked** | **bool**| | [optional] + **withPartners** | **bool**| | [optional] **key** | **String**| | [optional] ### Return type @@ -1151,9 +1212,7 @@ Name | Type | Description | Notes # **getUserAssetsByDeviceId** > List getUserAssetsByDeviceId(deviceId) - - -Get all asset of a device that are in the database, ID only. +Use /asset/device/:deviceId instead - Remove in 1.92 release ### Example ```dart @@ -1174,7 +1233,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = AssetApi(); -final deviceId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final deviceId = deviceId_example; // String | try { final result = api_instance.getUserAssetsByDeviceId(deviceId); @@ -1205,61 +1264,6 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **importFile** -> AssetFileUploadResponseDto importFile(importAssetDto) - - - -### Example -```dart -import 'package:openapi/api.dart'; -// TODO Configure API key authorization: cookie -//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; -// TODO Configure API key authorization: api_key -//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; -// uncomment below to setup prefix (e.g. Bearer) for API key, if needed -//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; -// TODO Configure HTTP Bearer authorization: bearer -// Case 1. Use String Token -//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); -// Case 2. Use Function which generate token. -// String yourTokenGeneratorFunction() { ... } -//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); - -final api_instance = AssetApi(); -final importAssetDto = ImportAssetDto(); // ImportAssetDto | - -try { - final result = api_instance.importFile(importAssetDto); - print(result); -} catch (e) { - print('Exception when calling AssetApi->importFile: $e\n'); -} -``` - -### Parameters - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **importAssetDto** | [**ImportAssetDto**](ImportAssetDto.md)| | - -### Return type - -[**AssetFileUploadResponseDto**](AssetFileUploadResponseDto.md) - -### Authorization - -[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) - -### HTTP request headers - - - **Content-Type**: application/json - - **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) - # **restoreAssets** > restoreAssets(bulkIdsDto) @@ -1418,8 +1422,8 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **searchAsset** -> List searchAsset(searchAssetDto) +# **searchAssets** +> List searchAssets(id, libraryId, type, order, deviceAssetId, deviceId, checksum, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, withDeleted, withStacked, withExif, withPeople, createdBefore, createdAfter, updatedBefore, updatedAfter, trashedBefore, trashedAfter, takenBefore, takenAfter, originalFileName, originalPath, resizePath, webpPath, encodedVideoPath, city, state, country, make, model, lensModel, page, size) @@ -1442,13 +1446,52 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = AssetApi(); -final searchAssetDto = SearchAssetDto(); // SearchAssetDto | +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final libraryId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final type = ; // AssetTypeEnum | +final order = ; // AssetOrder | +final deviceAssetId = deviceAssetId_example; // String | +final deviceId = deviceId_example; // String | +final checksum = checksum_example; // String | +final isArchived = true; // bool | +final isEncoded = true; // bool | +final isExternal = true; // bool | +final isFavorite = true; // bool | +final isMotion = true; // bool | +final isOffline = true; // bool | +final isReadOnly = true; // bool | +final isVisible = true; // bool | +final withDeleted = true; // bool | +final withStacked = true; // bool | +final withExif = true; // bool | +final withPeople = true; // bool | +final createdBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final createdAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final updatedBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final updatedAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final trashedBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final trashedAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final takenBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final takenAfter = 2013-10-20T19:20:30+01:00; // DateTime | +final originalFileName = originalFileName_example; // String | +final originalPath = originalPath_example; // String | +final resizePath = resizePath_example; // String | +final webpPath = webpPath_example; // String | +final encodedVideoPath = encodedVideoPath_example; // String | +final city = city_example; // String | +final state = state_example; // String | +final country = country_example; // String | +final make = make_example; // String | +final model = model_example; // String | +final lensModel = lensModel_example; // String | +final page = 8.14; // num | +final size = 8.14; // num | try { - final result = api_instance.searchAsset(searchAssetDto); + final result = api_instance.searchAssets(id, libraryId, type, order, deviceAssetId, deviceId, checksum, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, withDeleted, withStacked, withExif, withPeople, createdBefore, createdAfter, updatedBefore, updatedAfter, trashedBefore, trashedAfter, takenBefore, takenAfter, originalFileName, originalPath, resizePath, webpPath, encodedVideoPath, city, state, country, make, model, lensModel, page, size); print(result); } catch (e) { - print('Exception when calling AssetApi->searchAsset: $e\n'); + print('Exception when calling AssetApi->searchAssets: $e\n'); } ``` @@ -1456,7 +1499,46 @@ try { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **searchAssetDto** | [**SearchAssetDto**](SearchAssetDto.md)| | + **id** | **String**| | [optional] + **libraryId** | **String**| | [optional] + **type** | [**AssetTypeEnum**](.md)| | [optional] + **order** | [**AssetOrder**](.md)| | [optional] + **deviceAssetId** | **String**| | [optional] + **deviceId** | **String**| | [optional] + **checksum** | **String**| | [optional] + **isArchived** | **bool**| | [optional] + **isEncoded** | **bool**| | [optional] + **isExternal** | **bool**| | [optional] + **isFavorite** | **bool**| | [optional] + **isMotion** | **bool**| | [optional] + **isOffline** | **bool**| | [optional] + **isReadOnly** | **bool**| | [optional] + **isVisible** | **bool**| | [optional] + **withDeleted** | **bool**| | [optional] + **withStacked** | **bool**| | [optional] + **withExif** | **bool**| | [optional] + **withPeople** | **bool**| | [optional] + **createdBefore** | **DateTime**| | [optional] + **createdAfter** | **DateTime**| | [optional] + **updatedBefore** | **DateTime**| | [optional] + **updatedAfter** | **DateTime**| | [optional] + **trashedBefore** | **DateTime**| | [optional] + **trashedAfter** | **DateTime**| | [optional] + **takenBefore** | **DateTime**| | [optional] + **takenAfter** | **DateTime**| | [optional] + **originalFileName** | **String**| | [optional] + **originalPath** | **String**| | [optional] + **resizePath** | **String**| | [optional] + **webpPath** | **String**| | [optional] + **encodedVideoPath** | **String**| | [optional] + **city** | **String**| | [optional] + **state** | **String**| | [optional] + **country** | **String**| | [optional] + **make** | **String**| | [optional] + **model** | **String**| | [optional] + **lensModel** | **String**| | [optional] + **page** | **num**| | [optional] + **size** | **num**| | [optional] ### Return type @@ -1468,7 +1550,7 @@ Name | Type | Description | Notes ### HTTP request headers - - **Content-Type**: application/json + - **Content-Type**: Not defined - **Accept**: application/json [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/AssetBulkUpdateDto.md b/mobile/openapi/doc/AssetBulkUpdateDto.md index 74fd5ec45..40ebe6a41 100644 --- a/mobile/openapi/doc/AssetBulkUpdateDto.md +++ b/mobile/openapi/doc/AssetBulkUpdateDto.md @@ -8,9 +8,12 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- +**dateTimeOriginal** | **String** | | [optional] **ids** | **List** | | [default to const []] **isArchived** | **bool** | | [optional] **isFavorite** | **bool** | | [optional] +**latitude** | **num** | | [optional] +**longitude** | **num** | | [optional] **removeParent** | **bool** | | [optional] **stackParentId** | **String** | | [optional] diff --git a/mobile/openapi/doc/CitiesFile.md b/mobile/openapi/doc/AssetOrder.md similarity index 92% rename from mobile/openapi/doc/CitiesFile.md rename to mobile/openapi/doc/AssetOrder.md index 9acca959c..d1460775e 100644 --- a/mobile/openapi/doc/CitiesFile.md +++ b/mobile/openapi/doc/AssetOrder.md @@ -1,4 +1,4 @@ -# openapi.model.CitiesFile +# openapi.model.AssetOrder ## Load the model package ```dart diff --git a/mobile/openapi/doc/AuthenticationApi.md b/mobile/openapi/doc/AuthenticationApi.md index c56f88228..9521568e9 100644 --- a/mobile/openapi/doc/AuthenticationApi.md +++ b/mobile/openapi/doc/AuthenticationApi.md @@ -322,7 +322,7 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **signUpAdmin** -> AdminSignupResponseDto signUpAdmin(signUpDto) +> UserResponseDto signUpAdmin(signUpDto) @@ -349,7 +349,7 @@ Name | Type | Description | Notes ### Return type -[**AdminSignupResponseDto**](AdminSignupResponseDto.md) +[**UserResponseDto**](UserResponseDto.md) ### Authorization diff --git a/mobile/openapi/doc/CreateUserDto.md b/mobile/openapi/doc/CreateUserDto.md index 0a25b4df4..08fa8665a 100644 --- a/mobile/openapi/doc/CreateUserDto.md +++ b/mobile/openapi/doc/CreateUserDto.md @@ -10,9 +10,8 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **email** | **String** | | **externalPath** | **String** | | [optional] -**firstName** | **String** | | -**lastName** | **String** | | **memoriesEnabled** | **bool** | | [optional] +**name** | **String** | | **password** | **String** | | **storageLabel** | **String** | | [optional] diff --git a/mobile/openapi/doc/ImportAssetDto.md b/mobile/openapi/doc/ImportAssetDto.md deleted file mode 100644 index 3f2747edc..000000000 --- a/mobile/openapi/doc/ImportAssetDto.md +++ /dev/null @@ -1,28 +0,0 @@ -# openapi.model.ImportAssetDto - -## Load the model package -```dart -import 'package:openapi/api.dart'; -``` - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**assetPath** | **String** | | -**deviceAssetId** | **String** | | -**deviceId** | **String** | | -**duration** | **String** | | [optional] -**fileCreatedAt** | [**DateTime**](DateTime.md) | | -**fileModifiedAt** | [**DateTime**](DateTime.md) | | -**isArchived** | **bool** | | [optional] -**isExternal** | **bool** | | [optional] -**isFavorite** | **bool** | | [optional] -**isOffline** | **bool** | | [optional] -**isReadOnly** | **bool** | | [optional] [default to true] -**isVisible** | **bool** | | [optional] -**libraryId** | **String** | | [optional] -**sidecarPath** | **String** | | [optional] - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/mobile/openapi/doc/LoginResponseDto.md b/mobile/openapi/doc/LoginResponseDto.md index ad3592eff..bd37dc420 100644 --- a/mobile/openapi/doc/LoginResponseDto.md +++ b/mobile/openapi/doc/LoginResponseDto.md @@ -8,14 +8,13 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**accessToken** | **String** | | [readonly] -**firstName** | **String** | | [readonly] -**isAdmin** | **bool** | | [readonly] -**lastName** | **String** | | [readonly] -**profileImagePath** | **String** | | [readonly] -**shouldChangePassword** | **bool** | | [readonly] -**userEmail** | **String** | | [readonly] -**userId** | **String** | | [readonly] +**accessToken** | **String** | | +**isAdmin** | **bool** | | +**name** | **String** | | +**profileImagePath** | **String** | | +**shouldChangePassword** | **bool** | | +**userEmail** | **String** | | +**userId** | **String** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/SearchAssetDto.md b/mobile/openapi/doc/MapTheme.md similarity index 84% rename from mobile/openapi/doc/SearchAssetDto.md rename to mobile/openapi/doc/MapTheme.md index 1eadd2a0c..29fd8d998 100644 --- a/mobile/openapi/doc/SearchAssetDto.md +++ b/mobile/openapi/doc/MapTheme.md @@ -1,4 +1,4 @@ -# openapi.model.SearchAssetDto +# openapi.model.MapTheme ## Load the model package ```dart @@ -8,7 +8,6 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**searchTerm** | **String** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/PartnerApi.md b/mobile/openapi/doc/PartnerApi.md index 937978bef..5b4624e0a 100644 --- a/mobile/openapi/doc/PartnerApi.md +++ b/mobile/openapi/doc/PartnerApi.md @@ -12,10 +12,11 @@ Method | HTTP request | Description [**createPartner**](PartnerApi.md#createpartner) | **POST** /partner/{id} | [**getPartners**](PartnerApi.md#getpartners) | **GET** /partner | [**removePartner**](PartnerApi.md#removepartner) | **DELETE** /partner/{id} | +[**updatePartner**](PartnerApi.md#updatepartner) | **PUT** /partner/{id} | # **createPartner** -> UserResponseDto createPartner(id) +> PartnerResponseDto createPartner(id) @@ -56,7 +57,7 @@ Name | Type | Description | Notes ### Return type -[**UserResponseDto**](UserResponseDto.md) +[**PartnerResponseDto**](PartnerResponseDto.md) ### Authorization @@ -70,7 +71,7 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getPartners** -> List getPartners(direction) +> List getPartners(direction) @@ -111,7 +112,7 @@ Name | Type | Description | Notes ### Return type -[**List**](UserResponseDto.md) +[**List**](PartnerResponseDto.md) ### Authorization @@ -178,3 +179,60 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **updatePartner** +> PartnerResponseDto updatePartner(id, updatePartnerDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = PartnerApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final updatePartnerDto = UpdatePartnerDto(); // UpdatePartnerDto | + +try { + final result = api_instance.updatePartner(id, updatePartnerDto); + print(result); +} catch (e) { + print('Exception when calling PartnerApi->updatePartner: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + **updatePartnerDto** | [**UpdatePartnerDto**](UpdatePartnerDto.md)| | + +### Return type + +[**PartnerResponseDto**](PartnerResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/mobile/openapi/doc/PartnerResponseDto.md b/mobile/openapi/doc/PartnerResponseDto.md new file mode 100644 index 000000000..574b96f8d --- /dev/null +++ b/mobile/openapi/doc/PartnerResponseDto.md @@ -0,0 +1,29 @@ +# openapi.model.PartnerResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**avatarColor** | [**UserAvatarColor**](UserAvatarColor.md) | | +**createdAt** | [**DateTime**](DateTime.md) | | +**deletedAt** | [**DateTime**](DateTime.md) | | +**email** | **String** | | +**externalPath** | **String** | | +**id** | **String** | | +**inTimeline** | **bool** | | [optional] +**isAdmin** | **bool** | | +**memoriesEnabled** | **bool** | | [optional] +**name** | **String** | | +**oauthId** | **String** | | +**profileImagePath** | **String** | | +**shouldChangePassword** | **bool** | | +**storageLabel** | **String** | | +**updatedAt** | [**DateTime**](DateTime.md) | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/AdminSignupResponseDto.md b/mobile/openapi/doc/ReactionLevel.md similarity index 62% rename from mobile/openapi/doc/AdminSignupResponseDto.md rename to mobile/openapi/doc/ReactionLevel.md index 08d3d8bfa..a53955cb0 100644 --- a/mobile/openapi/doc/AdminSignupResponseDto.md +++ b/mobile/openapi/doc/ReactionLevel.md @@ -1,4 +1,4 @@ -# openapi.model.AdminSignupResponseDto +# openapi.model.ReactionLevel ## Load the model package ```dart @@ -8,11 +8,6 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**createdAt** | [**DateTime**](DateTime.md) | | -**email** | **String** | | -**firstName** | **String** | | -**id** | **String** | | -**lastName** | **String** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/ServerConfigDto.md b/mobile/openapi/doc/ServerConfigDto.md index cbe56d2f4..fd543257b 100644 --- a/mobile/openapi/doc/ServerConfigDto.md +++ b/mobile/openapi/doc/ServerConfigDto.md @@ -10,7 +10,6 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **isInitialized** | **bool** | | **loginPageMessage** | **String** | | -**mapTileUrl** | **String** | | **oauthButtonText** | **String** | | **trashDays** | **int** | | diff --git a/mobile/openapi/doc/SignUpDto.md b/mobile/openapi/doc/SignUpDto.md index 92294df02..9706c7c54 100644 --- a/mobile/openapi/doc/SignUpDto.md +++ b/mobile/openapi/doc/SignUpDto.md @@ -9,8 +9,7 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **email** | **String** | | -**firstName** | **String** | | -**lastName** | **String** | | +**name** | **String** | | **password** | **String** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/SystemConfigApi.md b/mobile/openapi/doc/SystemConfigApi.md index 9377b6e0b..e782265b4 100644 --- a/mobile/openapi/doc/SystemConfigApi.md +++ b/mobile/openapi/doc/SystemConfigApi.md @@ -11,6 +11,7 @@ Method | HTTP request | Description ------------- | ------------- | ------------- [**getConfig**](SystemConfigApi.md#getconfig) | **GET** /system-config | [**getConfigDefaults**](SystemConfigApi.md#getconfigdefaults) | **GET** /system-config/defaults | +[**getMapStyle**](SystemConfigApi.md#getmapstyle) | **GET** /system-config/map/style.json | [**getStorageTemplateOptions**](SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options | [**updateConfig**](SystemConfigApi.md#updateconfig) | **PUT** /system-config | @@ -117,6 +118,61 @@ This endpoint does not need any parameter. [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **getMapStyle** +> Object getMapStyle(theme) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SystemConfigApi(); +final theme = ; // MapTheme | + +try { + final result = api_instance.getMapStyle(theme); + print(result); +} catch (e) { + print('Exception when calling SystemConfigApi->getMapStyle: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **theme** | [**MapTheme**](.md)| | + +### Return type + +[**Object**](Object.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **getStorageTemplateOptions** > SystemConfigTemplateStorageOptionDto getStorageTemplateOptions() diff --git a/mobile/openapi/doc/SystemConfigMapDto.md b/mobile/openapi/doc/SystemConfigMapDto.md index 99fa91fd2..1846563eb 100644 --- a/mobile/openapi/doc/SystemConfigMapDto.md +++ b/mobile/openapi/doc/SystemConfigMapDto.md @@ -8,8 +8,9 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- +**darkStyle** | **String** | | **enabled** | **bool** | | -**tileUrl** | **String** | | +**lightStyle** | **String** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/SystemConfigReverseGeocodingDto.md b/mobile/openapi/doc/SystemConfigReverseGeocodingDto.md index 36eab4747..9fca6c209 100644 --- a/mobile/openapi/doc/SystemConfigReverseGeocodingDto.md +++ b/mobile/openapi/doc/SystemConfigReverseGeocodingDto.md @@ -8,7 +8,6 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**citiesFileOverride** | [**CitiesFile**](CitiesFile.md) | | **enabled** | **bool** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/UpdateAlbumDto.md b/mobile/openapi/doc/UpdateAlbumDto.md index 283b8bc29..4ded87d1b 100644 --- a/mobile/openapi/doc/UpdateAlbumDto.md +++ b/mobile/openapi/doc/UpdateAlbumDto.md @@ -11,6 +11,7 @@ Name | Type | Description | Notes **albumName** | **String** | | [optional] **albumThumbnailAssetId** | **String** | | [optional] **description** | **String** | | [optional] +**isActivityEnabled** | **bool** | | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/UpdateAssetDto.md b/mobile/openapi/doc/UpdateAssetDto.md index d214ebd47..cfd8f604d 100644 --- a/mobile/openapi/doc/UpdateAssetDto.md +++ b/mobile/openapi/doc/UpdateAssetDto.md @@ -8,9 +8,12 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- +**dateTimeOriginal** | **String** | | [optional] **description** | **String** | | [optional] **isArchived** | **bool** | | [optional] **isFavorite** | **bool** | | [optional] +**latitude** | **num** | | [optional] +**longitude** | **num** | | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/UpdatePartnerDto.md b/mobile/openapi/doc/UpdatePartnerDto.md new file mode 100644 index 000000000..b336c419e --- /dev/null +++ b/mobile/openapi/doc/UpdatePartnerDto.md @@ -0,0 +1,15 @@ +# openapi.model.UpdatePartnerDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**inTimeline** | **bool** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/UpdateUserDto.md b/mobile/openapi/doc/UpdateUserDto.md index ff6bc8d42..567bc43eb 100644 --- a/mobile/openapi/doc/UpdateUserDto.md +++ b/mobile/openapi/doc/UpdateUserDto.md @@ -8,13 +8,13 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- +**avatarColor** | [**UserAvatarColor**](UserAvatarColor.md) | | [optional] **email** | **String** | | [optional] **externalPath** | **String** | | [optional] -**firstName** | **String** | | [optional] **id** | **String** | | **isAdmin** | **bool** | | [optional] -**lastName** | **String** | | [optional] **memoriesEnabled** | **bool** | | [optional] +**name** | **String** | | [optional] **password** | **String** | | [optional] **shouldChangePassword** | **bool** | | [optional] **storageLabel** | **String** | | [optional] diff --git a/mobile/openapi/doc/UsageByUserDto.md b/mobile/openapi/doc/UsageByUserDto.md index c1c5240b9..f7b1b3593 100644 --- a/mobile/openapi/doc/UsageByUserDto.md +++ b/mobile/openapi/doc/UsageByUserDto.md @@ -10,9 +10,8 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **photos** | **int** | | **usage** | **int** | | -**userFirstName** | **String** | | **userId** | **String** | | -**userLastName** | **String** | | +**userName** | **String** | | **videos** | **int** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/UserApi.md b/mobile/openapi/doc/UserApi.md index fb88d53bd..6d2a028da 100644 --- a/mobile/openapi/doc/UserApi.md +++ b/mobile/openapi/doc/UserApi.md @@ -11,6 +11,7 @@ Method | HTTP request | Description ------------- | ------------- | ------------- [**createProfileImage**](UserApi.md#createprofileimage) | **POST** /user/profile-image | [**createUser**](UserApi.md#createuser) | **POST** /user | +[**deleteProfileImage**](UserApi.md#deleteprofileimage) | **DELETE** /user/profile-image | [**deleteUser**](UserApi.md#deleteuser) | **DELETE** /user/{id} | [**getAllUsers**](UserApi.md#getallusers) | **GET** /user | [**getMyUserInfo**](UserApi.md#getmyuserinfo) | **GET** /user/me | @@ -130,6 +131,56 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **deleteProfileImage** +> deleteProfileImage() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = UserApi(); + +try { + api_instance.deleteProfileImage(); +} catch (e) { + print('Exception when calling UserApi->deleteProfileImage: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **deleteUser** > UserResponseDto deleteUser(id) diff --git a/mobile/openapi/doc/UserAvatarColor.md b/mobile/openapi/doc/UserAvatarColor.md new file mode 100644 index 000000000..a07350de1 --- /dev/null +++ b/mobile/openapi/doc/UserAvatarColor.md @@ -0,0 +1,14 @@ +# openapi.model.UserAvatarColor + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/UserDto.md b/mobile/openapi/doc/UserDto.md index 617ceb9d3..7e5770f84 100644 --- a/mobile/openapi/doc/UserDto.md +++ b/mobile/openapi/doc/UserDto.md @@ -8,10 +8,10 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- +**avatarColor** | [**UserAvatarColor**](UserAvatarColor.md) | | **email** | **String** | | -**firstName** | **String** | | **id** | **String** | | -**lastName** | **String** | | +**name** | **String** | | **profileImagePath** | **String** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/UserResponseDto.md b/mobile/openapi/doc/UserResponseDto.md index b6e42b33e..93f9aa62a 100644 --- a/mobile/openapi/doc/UserResponseDto.md +++ b/mobile/openapi/doc/UserResponseDto.md @@ -8,15 +8,15 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- +**avatarColor** | [**UserAvatarColor**](UserAvatarColor.md) | | **createdAt** | [**DateTime**](DateTime.md) | | **deletedAt** | [**DateTime**](DateTime.md) | | **email** | **String** | | **externalPath** | **String** | | -**firstName** | **String** | | **id** | **String** | | **isAdmin** | **bool** | | -**lastName** | **String** | | **memoriesEnabled** | **bool** | | [optional] +**name** | **String** | | **oauthId** | **String** | | **profileImagePath** | **String** | | **shouldChangePassword** | **bool** | | diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 2bd437010..894162693 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -54,7 +54,6 @@ part 'model/activity_create_dto.dart'; part 'model/activity_response_dto.dart'; part 'model/activity_statistics_response_dto.dart'; part 'model/add_users_dto.dart'; -part 'model/admin_signup_response_dto.dart'; part 'model/album_count_response_dto.dart'; part 'model/album_response_dto.dart'; part 'model/all_job_status_response_dto.dart'; @@ -69,6 +68,7 @@ part 'model/asset_ids_dto.dart'; part 'model/asset_ids_response_dto.dart'; part 'model/asset_job_name.dart'; part 'model/asset_jobs_dto.dart'; +part 'model/asset_order.dart'; part 'model/asset_response_dto.dart'; part 'model/asset_stats_response_dto.dart'; part 'model/asset_type_enum.dart'; @@ -83,7 +83,6 @@ part 'model/cq_mode.dart'; part 'model/change_password_dto.dart'; part 'model/check_existing_assets_dto.dart'; part 'model/check_existing_assets_response_dto.dart'; -part 'model/cities_file.dart'; part 'model/classification_config.dart'; part 'model/colorspace.dart'; part 'model/create_album_dto.dart'; @@ -103,7 +102,6 @@ part 'model/file_checksum_response_dto.dart'; part 'model/file_report_dto.dart'; part 'model/file_report_fix_dto.dart'; part 'model/file_report_item_dto.dart'; -part 'model/import_asset_dto.dart'; part 'model/job_command.dart'; part 'model/job_command_dto.dart'; part 'model/job_counts_dto.dart'; @@ -117,6 +115,7 @@ part 'model/login_credential_dto.dart'; part 'model/login_response_dto.dart'; part 'model/logout_response_dto.dart'; part 'model/map_marker_response_dto.dart'; +part 'model/map_theme.dart'; part 'model/memory_lane_response_dto.dart'; part 'model/merge_person_dto.dart'; part 'model/model_type.dart'; @@ -124,6 +123,7 @@ part 'model/o_auth_authorize_response_dto.dart'; part 'model/o_auth_callback_dto.dart'; part 'model/o_auth_config_dto.dart'; part 'model/o_auth_config_response_dto.dart'; +part 'model/partner_response_dto.dart'; part 'model/path_entity_type.dart'; part 'model/path_type.dart'; part 'model/people_response_dto.dart'; @@ -133,11 +133,11 @@ part 'model/person_response_dto.dart'; part 'model/person_statistics_response_dto.dart'; part 'model/person_update_dto.dart'; part 'model/queue_status_dto.dart'; +part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; part 'model/recognition_config.dart'; part 'model/scan_library_dto.dart'; part 'model/search_album_response_dto.dart'; -part 'model/search_asset_dto.dart'; part 'model/search_asset_response_dto.dart'; part 'model/search_explore_item.dart'; part 'model/search_explore_response_dto.dart'; @@ -185,10 +185,12 @@ part 'model/transcode_policy.dart'; part 'model/update_album_dto.dart'; part 'model/update_asset_dto.dart'; part 'model/update_library_dto.dart'; +part 'model/update_partner_dto.dart'; part 'model/update_stack_parent_dto.dart'; part 'model/update_tag_dto.dart'; part 'model/update_user_dto.dart'; part 'model/usage_by_user_dto.dart'; +part 'model/user_avatar_color.dart'; part 'model/user_dto.dart'; part 'model/user_response_dto.dart'; part 'model/validate_access_token_response_dto.dart'; diff --git a/mobile/openapi/lib/api/activity_api.dart b/mobile/openapi/lib/api/activity_api.dart index 458538a5d..8e2354e20 100644 --- a/mobile/openapi/lib/api/activity_api.dart +++ b/mobile/openapi/lib/api/activity_api.dart @@ -112,8 +112,10 @@ class ActivityApi { /// /// * [ReactionType] type: /// + /// * [ReactionLevel] level: + /// /// * [String] userId: - Future getActivitiesWithHttpInfo(String albumId, { String? assetId, ReactionType? type, String? userId, }) async { + Future getActivitiesWithHttpInfo(String albumId, { String? assetId, ReactionType? type, ReactionLevel? level, String? userId, }) async { // ignore: prefer_const_declarations final path = r'/activity'; @@ -131,6 +133,9 @@ class ActivityApi { if (type != null) { queryParams.addAll(_queryParams('', 'type', type)); } + if (level != null) { + queryParams.addAll(_queryParams('', 'level', level)); + } if (userId != null) { queryParams.addAll(_queryParams('', 'userId', userId)); } @@ -157,9 +162,11 @@ class ActivityApi { /// /// * [ReactionType] type: /// + /// * [ReactionLevel] level: + /// /// * [String] userId: - Future?> getActivities(String albumId, { String? assetId, ReactionType? type, String? userId, }) async { - final response = await getActivitiesWithHttpInfo(albumId, assetId: assetId, type: type, userId: userId, ); + Future?> getActivities(String albumId, { String? assetId, ReactionType? type, ReactionLevel? level, String? userId, }) async { + final response = await getActivitiesWithHttpInfo(albumId, assetId: assetId, type: type, level: level, userId: userId, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 0d3c2bfe8..45c1e1104 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -414,6 +414,62 @@ class AssetApi { return null; } + /// Get all asset of a device that are in the database, ID only. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] deviceId (required): + Future getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async { + // ignore: prefer_const_declarations + final path = r'/asset/device/{deviceId}' + .replaceAll('{deviceId}', deviceId); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Get all asset of a device that are in the database, ID only. + /// + /// Parameters: + /// + /// * [String] deviceId (required): + Future?> getAllUserAssetsByDeviceId(String deviceId,) async { + final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(); + + } + return null; + } + /// Get a single asset's information /// /// Note: This method returns the HTTP [Response]. @@ -997,8 +1053,10 @@ class AssetApi { /// /// * [bool] withStacked: /// + /// * [bool] withPartners: + /// /// * [String] key: - Future getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, bool? withStacked, String? key, }) async { + Future getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, bool? withStacked, bool? withPartners, String? key, }) async { // ignore: prefer_const_declarations final path = r'/asset/time-bucket'; @@ -1030,6 +1088,9 @@ class AssetApi { } if (withStacked != null) { queryParams.addAll(_queryParams('', 'withStacked', withStacked)); + } + if (withPartners != null) { + queryParams.addAll(_queryParams('', 'withPartners', withPartners)); } queryParams.addAll(_queryParams('', 'timeBucket', timeBucket)); if (key != null) { @@ -1070,9 +1131,11 @@ class AssetApi { /// /// * [bool] withStacked: /// + /// * [bool] withPartners: + /// /// * [String] key: - Future?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, bool? withStacked, String? key, }) async { - final response = await getTimeBucketWithHttpInfo(size, timeBucket, userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, withStacked: withStacked, key: key, ); + Future?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, bool? withStacked, bool? withPartners, String? key, }) async { + final response = await getTimeBucketWithHttpInfo(size, timeBucket, userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, withStacked: withStacked, withPartners: withPartners, key: key, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1108,8 +1171,10 @@ class AssetApi { /// /// * [bool] withStacked: /// + /// * [bool] withPartners: + /// /// * [String] key: - Future getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, bool? withStacked, String? key, }) async { + Future getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, bool? withStacked, bool? withPartners, String? key, }) async { // ignore: prefer_const_declarations final path = r'/asset/time-buckets'; @@ -1142,6 +1207,9 @@ class AssetApi { if (withStacked != null) { queryParams.addAll(_queryParams('', 'withStacked', withStacked)); } + if (withPartners != null) { + queryParams.addAll(_queryParams('', 'withPartners', withPartners)); + } if (key != null) { queryParams.addAll(_queryParams('', 'key', key)); } @@ -1178,9 +1246,11 @@ class AssetApi { /// /// * [bool] withStacked: /// + /// * [bool] withPartners: + /// /// * [String] key: - Future?> getTimeBuckets(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, bool? withStacked, String? key, }) async { - final response = await getTimeBucketsWithHttpInfo(size, userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, withStacked: withStacked, key: key, ); + Future?> getTimeBuckets(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, bool? withStacked, bool? withPartners, String? key, }) async { + final response = await getTimeBucketsWithHttpInfo(size, userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, withStacked: withStacked, withPartners: withPartners, key: key, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -1197,7 +1267,7 @@ class AssetApi { return null; } - /// Get all asset of a device that are in the database, ID only. + /// Use /asset/device/:deviceId instead - Remove in 1.92 release /// /// Note: This method returns the HTTP [Response]. /// @@ -1230,7 +1300,7 @@ class AssetApi { ); } - /// Get all asset of a device that are in the database, ID only. + /// Use /asset/device/:deviceId instead - Remove in 1.92 release /// /// Parameters: /// @@ -1253,53 +1323,6 @@ class AssetApi { return null; } - /// Performs an HTTP 'POST /asset/import' operation and returns the [Response]. - /// Parameters: - /// - /// * [ImportAssetDto] importAssetDto (required): - Future importFileWithHttpInfo(ImportAssetDto importAssetDto,) async { - // ignore: prefer_const_declarations - final path = r'/asset/import'; - - // ignore: prefer_final_locals - Object? postBody = importAssetDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [ImportAssetDto] importAssetDto (required): - Future importFile(ImportAssetDto importAssetDto,) async { - final response = await importFileWithHttpInfo(importAssetDto,); - 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), 'AssetFileUploadResponseDto',) as AssetFileUploadResponseDto; - - } - return null; - } - /// Performs an HTTP 'POST /asset/restore' operation and returns the [Response]. /// Parameters: /// @@ -1411,27 +1434,226 @@ class AssetApi { } } - /// Performs an HTTP 'POST /asset/search' operation and returns the [Response]. + /// Performs an HTTP 'GET /assets' operation and returns the [Response]. /// Parameters: /// - /// * [SearchAssetDto] searchAssetDto (required): - Future searchAssetWithHttpInfo(SearchAssetDto searchAssetDto,) async { + /// * [String] id: + /// + /// * [String] libraryId: + /// + /// * [AssetTypeEnum] type: + /// + /// * [AssetOrder] order: + /// + /// * [String] deviceAssetId: + /// + /// * [String] deviceId: + /// + /// * [String] checksum: + /// + /// * [bool] isArchived: + /// + /// * [bool] isEncoded: + /// + /// * [bool] isExternal: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isMotion: + /// + /// * [bool] isOffline: + /// + /// * [bool] isReadOnly: + /// + /// * [bool] isVisible: + /// + /// * [bool] withDeleted: + /// + /// * [bool] withStacked: + /// + /// * [bool] withExif: + /// + /// * [bool] withPeople: + /// + /// * [DateTime] createdBefore: + /// + /// * [DateTime] createdAfter: + /// + /// * [DateTime] updatedBefore: + /// + /// * [DateTime] updatedAfter: + /// + /// * [DateTime] trashedBefore: + /// + /// * [DateTime] trashedAfter: + /// + /// * [DateTime] takenBefore: + /// + /// * [DateTime] takenAfter: + /// + /// * [String] originalFileName: + /// + /// * [String] originalPath: + /// + /// * [String] resizePath: + /// + /// * [String] webpPath: + /// + /// * [String] encodedVideoPath: + /// + /// * [String] city: + /// + /// * [String] state: + /// + /// * [String] country: + /// + /// * [String] make: + /// + /// * [String] model: + /// + /// * [String] lensModel: + /// + /// * [num] page: + /// + /// * [num] size: + Future searchAssetsWithHttpInfo({ String? id, String? libraryId, AssetTypeEnum? type, AssetOrder? order, String? deviceAssetId, String? deviceId, String? checksum, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, bool? withDeleted, bool? withStacked, bool? withExif, bool? withPeople, DateTime? createdBefore, DateTime? createdAfter, DateTime? updatedBefore, DateTime? updatedAfter, DateTime? trashedBefore, DateTime? trashedAfter, DateTime? takenBefore, DateTime? takenAfter, String? originalFileName, String? originalPath, String? resizePath, String? webpPath, String? encodedVideoPath, String? city, String? state, String? country, String? make, String? model, String? lensModel, num? page, num? size, }) async { // ignore: prefer_const_declarations - final path = r'/asset/search'; + final path = r'/assets'; // ignore: prefer_final_locals - Object? postBody = searchAssetDto; + Object? postBody; final queryParams = []; final headerParams = {}; final formParams = {}; - const contentTypes = ['application/json']; + if (id != null) { + queryParams.addAll(_queryParams('', 'id', id)); + } + if (libraryId != null) { + queryParams.addAll(_queryParams('', 'libraryId', libraryId)); + } + if (type != null) { + queryParams.addAll(_queryParams('', 'type', type)); + } + if (order != null) { + queryParams.addAll(_queryParams('', 'order', order)); + } + if (deviceAssetId != null) { + queryParams.addAll(_queryParams('', 'deviceAssetId', deviceAssetId)); + } + if (deviceId != null) { + queryParams.addAll(_queryParams('', 'deviceId', deviceId)); + } + if (checksum != null) { + queryParams.addAll(_queryParams('', 'checksum', checksum)); + } + if (isArchived != null) { + queryParams.addAll(_queryParams('', 'isArchived', isArchived)); + } + if (isEncoded != null) { + queryParams.addAll(_queryParams('', 'isEncoded', isEncoded)); + } + if (isExternal != null) { + queryParams.addAll(_queryParams('', 'isExternal', isExternal)); + } + if (isFavorite != null) { + queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); + } + if (isMotion != null) { + queryParams.addAll(_queryParams('', 'isMotion', isMotion)); + } + if (isOffline != null) { + queryParams.addAll(_queryParams('', 'isOffline', isOffline)); + } + if (isReadOnly != null) { + queryParams.addAll(_queryParams('', 'isReadOnly', isReadOnly)); + } + if (isVisible != null) { + queryParams.addAll(_queryParams('', 'isVisible', isVisible)); + } + if (withDeleted != null) { + queryParams.addAll(_queryParams('', 'withDeleted', withDeleted)); + } + if (withStacked != null) { + queryParams.addAll(_queryParams('', 'withStacked', withStacked)); + } + if (withExif != null) { + queryParams.addAll(_queryParams('', 'withExif', withExif)); + } + if (withPeople != null) { + queryParams.addAll(_queryParams('', 'withPeople', withPeople)); + } + if (createdBefore != null) { + queryParams.addAll(_queryParams('', 'createdBefore', createdBefore)); + } + if (createdAfter != null) { + queryParams.addAll(_queryParams('', 'createdAfter', createdAfter)); + } + if (updatedBefore != null) { + queryParams.addAll(_queryParams('', 'updatedBefore', updatedBefore)); + } + if (updatedAfter != null) { + queryParams.addAll(_queryParams('', 'updatedAfter', updatedAfter)); + } + if (trashedBefore != null) { + queryParams.addAll(_queryParams('', 'trashedBefore', trashedBefore)); + } + if (trashedAfter != null) { + queryParams.addAll(_queryParams('', 'trashedAfter', trashedAfter)); + } + if (takenBefore != null) { + queryParams.addAll(_queryParams('', 'takenBefore', takenBefore)); + } + if (takenAfter != null) { + queryParams.addAll(_queryParams('', 'takenAfter', takenAfter)); + } + if (originalFileName != null) { + queryParams.addAll(_queryParams('', 'originalFileName', originalFileName)); + } + if (originalPath != null) { + queryParams.addAll(_queryParams('', 'originalPath', originalPath)); + } + if (resizePath != null) { + queryParams.addAll(_queryParams('', 'resizePath', resizePath)); + } + if (webpPath != null) { + queryParams.addAll(_queryParams('', 'webpPath', webpPath)); + } + if (encodedVideoPath != null) { + queryParams.addAll(_queryParams('', 'encodedVideoPath', encodedVideoPath)); + } + if (city != null) { + queryParams.addAll(_queryParams('', 'city', city)); + } + if (state != null) { + queryParams.addAll(_queryParams('', 'state', state)); + } + if (country != null) { + queryParams.addAll(_queryParams('', 'country', country)); + } + if (make != null) { + queryParams.addAll(_queryParams('', 'make', make)); + } + if (model != null) { + queryParams.addAll(_queryParams('', 'model', model)); + } + if (lensModel != null) { + queryParams.addAll(_queryParams('', 'lensModel', lensModel)); + } + if (page != null) { + queryParams.addAll(_queryParams('', 'page', page)); + } + if (size != null) { + queryParams.addAll(_queryParams('', 'size', size)); + } + + const contentTypes = []; return apiClient.invokeAPI( path, - 'POST', + 'GET', queryParams, postBody, headerParams, @@ -1442,9 +1664,87 @@ class AssetApi { /// Parameters: /// - /// * [SearchAssetDto] searchAssetDto (required): - Future?> searchAsset(SearchAssetDto searchAssetDto,) async { - final response = await searchAssetWithHttpInfo(searchAssetDto,); + /// * [String] id: + /// + /// * [String] libraryId: + /// + /// * [AssetTypeEnum] type: + /// + /// * [AssetOrder] order: + /// + /// * [String] deviceAssetId: + /// + /// * [String] deviceId: + /// + /// * [String] checksum: + /// + /// * [bool] isArchived: + /// + /// * [bool] isEncoded: + /// + /// * [bool] isExternal: + /// + /// * [bool] isFavorite: + /// + /// * [bool] isMotion: + /// + /// * [bool] isOffline: + /// + /// * [bool] isReadOnly: + /// + /// * [bool] isVisible: + /// + /// * [bool] withDeleted: + /// + /// * [bool] withStacked: + /// + /// * [bool] withExif: + /// + /// * [bool] withPeople: + /// + /// * [DateTime] createdBefore: + /// + /// * [DateTime] createdAfter: + /// + /// * [DateTime] updatedBefore: + /// + /// * [DateTime] updatedAfter: + /// + /// * [DateTime] trashedBefore: + /// + /// * [DateTime] trashedAfter: + /// + /// * [DateTime] takenBefore: + /// + /// * [DateTime] takenAfter: + /// + /// * [String] originalFileName: + /// + /// * [String] originalPath: + /// + /// * [String] resizePath: + /// + /// * [String] webpPath: + /// + /// * [String] encodedVideoPath: + /// + /// * [String] city: + /// + /// * [String] state: + /// + /// * [String] country: + /// + /// * [String] make: + /// + /// * [String] model: + /// + /// * [String] lensModel: + /// + /// * [num] page: + /// + /// * [num] size: + Future?> searchAssets({ String? id, String? libraryId, AssetTypeEnum? type, AssetOrder? order, String? deviceAssetId, String? deviceId, String? checksum, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isOffline, bool? isReadOnly, bool? isVisible, bool? withDeleted, bool? withStacked, bool? withExif, bool? withPeople, DateTime? createdBefore, DateTime? createdAfter, DateTime? updatedBefore, DateTime? updatedAfter, DateTime? trashedBefore, DateTime? trashedAfter, DateTime? takenBefore, DateTime? takenAfter, String? originalFileName, String? originalPath, String? resizePath, String? webpPath, String? encodedVideoPath, String? city, String? state, String? country, String? make, String? model, String? lensModel, num? page, num? size, }) async { + final response = await searchAssetsWithHttpInfo( id: id, libraryId: libraryId, type: type, order: order, deviceAssetId: deviceAssetId, deviceId: deviceId, checksum: checksum, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, withDeleted: withDeleted, withStacked: withStacked, withExif: withExif, withPeople: withPeople, createdBefore: createdBefore, createdAfter: createdAfter, updatedBefore: updatedBefore, updatedAfter: updatedAfter, trashedBefore: trashedBefore, trashedAfter: trashedAfter, takenBefore: takenBefore, takenAfter: takenAfter, originalFileName: originalFileName, originalPath: originalPath, resizePath: resizePath, webpPath: webpPath, encodedVideoPath: encodedVideoPath, city: city, state: state, country: country, make: make, model: model, lensModel: lensModel, page: page, size: size, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/authentication_api.dart b/mobile/openapi/lib/api/authentication_api.dart index 3b986d34d..7c6dac147 100644 --- a/mobile/openapi/lib/api/authentication_api.dart +++ b/mobile/openapi/lib/api/authentication_api.dart @@ -300,7 +300,7 @@ class AuthenticationApi { /// Parameters: /// /// * [SignUpDto] signUpDto (required): - Future signUpAdmin(SignUpDto signUpDto,) async { + Future signUpAdmin(SignUpDto signUpDto,) async { final response = await signUpAdminWithHttpInfo(signUpDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -309,7 +309,7 @@ class AuthenticationApi { // 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), 'AdminSignupResponseDto',) as AdminSignupResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserResponseDto',) as UserResponseDto; } return null; diff --git a/mobile/openapi/lib/api/partner_api.dart b/mobile/openapi/lib/api/partner_api.dart index cf374aafe..886b37a5b 100644 --- a/mobile/openapi/lib/api/partner_api.dart +++ b/mobile/openapi/lib/api/partner_api.dart @@ -49,7 +49,7 @@ class PartnerApi { /// Parameters: /// /// * [String] id (required): - Future createPartner(String id,) async { + Future createPartner(String id,) async { final response = await createPartnerWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -58,7 +58,7 @@ class PartnerApi { // 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), 'UserResponseDto',) as UserResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PartnerResponseDto',) as PartnerResponseDto; } return null; @@ -98,7 +98,7 @@ class PartnerApi { /// Parameters: /// /// * [String] direction (required): - Future?> getPartners(String direction,) async { + Future?> getPartners(String direction,) async { final response = await getPartnersWithHttpInfo(direction,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -108,8 +108,8 @@ class PartnerApi { // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() .toList(); } @@ -155,4 +155,56 @@ class PartnerApi { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } } + + /// Performs an HTTP 'PUT /partner/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [UpdatePartnerDto] updatePartnerDto (required): + Future updatePartnerWithHttpInfo(String id, UpdatePartnerDto updatePartnerDto,) async { + // ignore: prefer_const_declarations + final path = r'/partner/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = updatePartnerDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [UpdatePartnerDto] updatePartnerDto (required): + Future updatePartner(String id, UpdatePartnerDto updatePartnerDto,) async { + final response = await updatePartnerWithHttpInfo(id, updatePartnerDto,); + 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), 'PartnerResponseDto',) as PartnerResponseDto; + + } + return null; + } } diff --git a/mobile/openapi/lib/api/system_config_api.dart b/mobile/openapi/lib/api/system_config_api.dart index c8b0c58ec..f13f8d52d 100644 --- a/mobile/openapi/lib/api/system_config_api.dart +++ b/mobile/openapi/lib/api/system_config_api.dart @@ -98,6 +98,55 @@ class SystemConfigApi { return null; } + /// Performs an HTTP 'GET /system-config/map/style.json' operation and returns the [Response]. + /// Parameters: + /// + /// * [MapTheme] theme (required): + Future getMapStyleWithHttpInfo(MapTheme theme,) async { + // ignore: prefer_const_declarations + final path = r'/system-config/map/style.json'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + queryParams.addAll(_queryParams('', 'theme', theme)); + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [MapTheme] theme (required): + Future getMapStyle(MapTheme theme,) async { + final response = await getMapStyleWithHttpInfo(theme,); + 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), 'Object',) as Object; + + } + return null; + } + /// Performs an HTTP 'GET /system-config/storage-template-options' operation and returns the [Response]. Future getStorageTemplateOptionsWithHttpInfo() async { // ignore: prefer_const_declarations diff --git a/mobile/openapi/lib/api/user_api.dart b/mobile/openapi/lib/api/user_api.dart index 23f25492c..26ab3dcd0 100644 --- a/mobile/openapi/lib/api/user_api.dart +++ b/mobile/openapi/lib/api/user_api.dart @@ -120,6 +120,39 @@ class UserApi { return null; } + /// Performs an HTTP 'DELETE /user/profile-image' operation and returns the [Response]. + Future deleteProfileImageWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/user/profile-image'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future deleteProfileImage() async { + final response = await deleteProfileImageWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Performs an HTTP 'DELETE /user/{id}' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 76a849d71..42a0e5cbb 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -197,8 +197,6 @@ class ApiClient { return ActivityStatisticsResponseDto.fromJson(value); case 'AddUsersDto': return AddUsersDto.fromJson(value); - case 'AdminSignupResponseDto': - return AdminSignupResponseDto.fromJson(value); case 'AlbumCountResponseDto': return AlbumCountResponseDto.fromJson(value); case 'AlbumResponseDto': @@ -227,6 +225,8 @@ class ApiClient { return AssetJobNameTypeTransformer().decode(value); case 'AssetJobsDto': return AssetJobsDto.fromJson(value); + case 'AssetOrder': + return AssetOrderTypeTransformer().decode(value); case 'AssetResponseDto': return AssetResponseDto.fromJson(value); case 'AssetStatsResponseDto': @@ -255,8 +255,6 @@ class ApiClient { return CheckExistingAssetsDto.fromJson(value); case 'CheckExistingAssetsResponseDto': return CheckExistingAssetsResponseDto.fromJson(value); - case 'CitiesFile': - return CitiesFileTypeTransformer().decode(value); case 'ClassificationConfig': return ClassificationConfig.fromJson(value); case 'Colorspace': @@ -295,8 +293,6 @@ class ApiClient { return FileReportFixDto.fromJson(value); case 'FileReportItemDto': return FileReportItemDto.fromJson(value); - case 'ImportAssetDto': - return ImportAssetDto.fromJson(value); case 'JobCommand': return JobCommandTypeTransformer().decode(value); case 'JobCommandDto': @@ -323,6 +319,8 @@ class ApiClient { return LogoutResponseDto.fromJson(value); case 'MapMarkerResponseDto': return MapMarkerResponseDto.fromJson(value); + case 'MapTheme': + return MapThemeTypeTransformer().decode(value); case 'MemoryLaneResponseDto': return MemoryLaneResponseDto.fromJson(value); case 'MergePersonDto': @@ -337,6 +335,8 @@ class ApiClient { return OAuthConfigDto.fromJson(value); case 'OAuthConfigResponseDto': return OAuthConfigResponseDto.fromJson(value); + case 'PartnerResponseDto': + return PartnerResponseDto.fromJson(value); case 'PathEntityType': return PathEntityTypeTypeTransformer().decode(value); case 'PathType': @@ -355,6 +355,8 @@ class ApiClient { return PersonUpdateDto.fromJson(value); case 'QueueStatusDto': return QueueStatusDto.fromJson(value); + case 'ReactionLevel': + return ReactionLevelTypeTransformer().decode(value); case 'ReactionType': return ReactionTypeTypeTransformer().decode(value); case 'RecognitionConfig': @@ -363,8 +365,6 @@ class ApiClient { return ScanLibraryDto.fromJson(value); case 'SearchAlbumResponseDto': return SearchAlbumResponseDto.fromJson(value); - case 'SearchAssetDto': - return SearchAssetDto.fromJson(value); case 'SearchAssetResponseDto': return SearchAssetResponseDto.fromJson(value); case 'SearchExploreItem': @@ -459,6 +459,8 @@ class ApiClient { return UpdateAssetDto.fromJson(value); case 'UpdateLibraryDto': return UpdateLibraryDto.fromJson(value); + case 'UpdatePartnerDto': + return UpdatePartnerDto.fromJson(value); case 'UpdateStackParentDto': return UpdateStackParentDto.fromJson(value); case 'UpdateTagDto': @@ -467,6 +469,8 @@ class ApiClient { return UpdateUserDto.fromJson(value); case 'UsageByUserDto': return UsageByUserDto.fromJson(value); + case 'UserAvatarColor': + return UserAvatarColorTypeTransformer().decode(value); case 'UserDto': return UserDto.fromJson(value); case 'UserResponseDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index da870ea99..728a4ed83 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 AssetJobName) { return AssetJobNameTypeTransformer().encode(value).toString(); } + if (value is AssetOrder) { + return AssetOrderTypeTransformer().encode(value).toString(); + } if (value is AssetTypeEnum) { return AssetTypeEnumTypeTransformer().encode(value).toString(); } @@ -70,9 +73,6 @@ String parameterToString(dynamic value) { if (value is CQMode) { return CQModeTypeTransformer().encode(value).toString(); } - if (value is CitiesFile) { - return CitiesFileTypeTransformer().encode(value).toString(); - } if (value is Colorspace) { return ColorspaceTypeTransformer().encode(value).toString(); } @@ -88,6 +88,9 @@ String parameterToString(dynamic value) { if (value is LibraryType) { return LibraryTypeTypeTransformer().encode(value).toString(); } + if (value is MapTheme) { + return MapThemeTypeTransformer().encode(value).toString(); + } if (value is ModelType) { return ModelTypeTypeTransformer().encode(value).toString(); } @@ -97,6 +100,9 @@ String parameterToString(dynamic value) { if (value is PathType) { return PathTypeTypeTransformer().encode(value).toString(); } + if (value is ReactionLevel) { + return ReactionLevelTypeTransformer().encode(value).toString(); + } if (value is ReactionType) { return ReactionTypeTypeTransformer().encode(value).toString(); } @@ -121,6 +127,9 @@ String parameterToString(dynamic value) { if (value is TranscodePolicy) { return TranscodePolicyTypeTransformer().encode(value).toString(); } + if (value is UserAvatarColor) { + return UserAvatarColorTypeTransformer().encode(value).toString(); + } if (value is VideoCodec) { return VideoCodecTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/admin_signup_response_dto.dart b/mobile/openapi/lib/model/admin_signup_response_dto.dart deleted file mode 100644 index 5e6764f23..000000000 --- a/mobile/openapi/lib/model/admin_signup_response_dto.dart +++ /dev/null @@ -1,130 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.12 - -// 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 AdminSignupResponseDto { - /// Returns a new [AdminSignupResponseDto] instance. - AdminSignupResponseDto({ - required this.createdAt, - required this.email, - required this.firstName, - required this.id, - required this.lastName, - }); - - DateTime createdAt; - - String email; - - String firstName; - - String id; - - String lastName; - - @override - bool operator ==(Object other) => identical(this, other) || other is AdminSignupResponseDto && - other.createdAt == createdAt && - other.email == email && - other.firstName == firstName && - other.id == id && - other.lastName == lastName; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (createdAt.hashCode) + - (email.hashCode) + - (firstName.hashCode) + - (id.hashCode) + - (lastName.hashCode); - - @override - String toString() => 'AdminSignupResponseDto[createdAt=$createdAt, email=$email, firstName=$firstName, id=$id, lastName=$lastName]'; - - Map toJson() { - final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); - json[r'email'] = this.email; - json[r'firstName'] = this.firstName; - json[r'id'] = this.id; - json[r'lastName'] = this.lastName; - return json; - } - - /// Returns a new [AdminSignupResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static AdminSignupResponseDto? fromJson(dynamic value) { - if (value is Map) { - final json = value.cast(); - - return AdminSignupResponseDto( - createdAt: mapDateTime(json, r'createdAt', '')!, - email: mapValueOfType(json, r'email')!, - firstName: mapValueOfType(json, r'firstName')!, - id: mapValueOfType(json, r'id')!, - lastName: mapValueOfType(json, r'lastName')!, - ); - } - 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 = AdminSignupResponseDto.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 = AdminSignupResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of AdminSignupResponseDto-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] = AdminSignupResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'createdAt', - 'email', - 'firstName', - 'id', - 'lastName', - }; -} - diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index cf2ad9252..86e009e33 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -22,6 +22,7 @@ class AlbumResponseDto { this.endDate, required this.hasSharedLink, required this.id, + required this.isActivityEnabled, this.lastModifiedAssetTimestamp, required this.owner, required this.ownerId, @@ -55,6 +56,8 @@ class AlbumResponseDto { String id; + bool isActivityEnabled; + /// /// Please note: 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 +95,7 @@ class AlbumResponseDto { other.endDate == endDate && other.hasSharedLink == hasSharedLink && other.id == id && + other.isActivityEnabled == isActivityEnabled && other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp && other.owner == owner && other.ownerId == ownerId && @@ -112,6 +116,7 @@ class AlbumResponseDto { (endDate == null ? 0 : endDate!.hashCode) + (hasSharedLink.hashCode) + (id.hashCode) + + (isActivityEnabled.hashCode) + (lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) + (owner.hashCode) + (ownerId.hashCode) + @@ -121,7 +126,7 @@ class AlbumResponseDto { (updatedAt.hashCode); @override - String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, owner=$owner, ownerId=$ownerId, shared=$shared, sharedUsers=$sharedUsers, startDate=$startDate, updatedAt=$updatedAt]'; + String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, owner=$owner, ownerId=$ownerId, shared=$shared, sharedUsers=$sharedUsers, startDate=$startDate, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -142,6 +147,7 @@ class AlbumResponseDto { } json[r'hasSharedLink'] = this.hasSharedLink; json[r'id'] = this.id; + json[r'isActivityEnabled'] = this.isActivityEnabled; if (this.lastModifiedAssetTimestamp != null) { json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String(); } else { @@ -177,6 +183,7 @@ class AlbumResponseDto { endDate: mapDateTime(json, r'endDate', ''), hasSharedLink: mapValueOfType(json, r'hasSharedLink')!, id: mapValueOfType(json, r'id')!, + isActivityEnabled: mapValueOfType(json, r'isActivityEnabled')!, lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', ''), owner: UserResponseDto.fromJson(json[r'owner'])!, ownerId: mapValueOfType(json, r'ownerId')!, @@ -239,6 +246,7 @@ class AlbumResponseDto { 'description', 'hasSharedLink', 'id', + 'isActivityEnabled', 'owner', 'ownerId', 'shared', diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index 64c8d1e7e..60cab8c74 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -13,13 +13,24 @@ part of openapi.api; class AssetBulkUpdateDto { /// Returns a new [AssetBulkUpdateDto] instance. AssetBulkUpdateDto({ + this.dateTimeOriginal, this.ids = const [], this.isArchived, this.isFavorite, + this.latitude, + this.longitude, this.removeParent, this.stackParentId, }); + /// + /// Please note: 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? dateTimeOriginal; + List ids; /// @@ -38,6 +49,22 @@ class AssetBulkUpdateDto { /// bool? isFavorite; + /// + /// Please note: 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? latitude; + + /// + /// Please note: 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? longitude; + /// /// Please note: 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,26 +83,37 @@ class AssetBulkUpdateDto { @override bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto && + other.dateTimeOriginal == dateTimeOriginal && other.ids == ids && other.isArchived == isArchived && other.isFavorite == isFavorite && + other.latitude == latitude && + other.longitude == longitude && other.removeParent == removeParent && other.stackParentId == stackParentId; @override int get hashCode => // ignore: unnecessary_parenthesis + (dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) + (ids.hashCode) + (isArchived == null ? 0 : isArchived!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + + (latitude == null ? 0 : latitude!.hashCode) + + (longitude == null ? 0 : longitude!.hashCode) + (removeParent == null ? 0 : removeParent!.hashCode) + (stackParentId == null ? 0 : stackParentId!.hashCode); @override - String toString() => 'AssetBulkUpdateDto[ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, removeParent=$removeParent, stackParentId=$stackParentId]'; + String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, removeParent=$removeParent, stackParentId=$stackParentId]'; Map toJson() { final json = {}; + if (this.dateTimeOriginal != null) { + json[r'dateTimeOriginal'] = this.dateTimeOriginal; + } else { + // json[r'dateTimeOriginal'] = null; + } json[r'ids'] = this.ids; if (this.isArchived != null) { json[r'isArchived'] = this.isArchived; @@ -87,6 +125,16 @@ class AssetBulkUpdateDto { } else { // json[r'isFavorite'] = null; } + if (this.latitude != null) { + json[r'latitude'] = this.latitude; + } else { + // json[r'latitude'] = null; + } + if (this.longitude != null) { + json[r'longitude'] = this.longitude; + } else { + // json[r'longitude'] = null; + } if (this.removeParent != null) { json[r'removeParent'] = this.removeParent; } else { @@ -108,11 +156,18 @@ class AssetBulkUpdateDto { final json = value.cast(); return AssetBulkUpdateDto( + dateTimeOriginal: mapValueOfType(json, r'dateTimeOriginal'), ids: json[r'ids'] is List ? (json[r'ids'] as List).cast() : const [], isArchived: mapValueOfType(json, r'isArchived'), isFavorite: mapValueOfType(json, r'isFavorite'), + latitude: json[r'latitude'] == null + ? null + : num.parse(json[r'latitude'].toString()), + longitude: json[r'longitude'] == null + ? null + : num.parse(json[r'longitude'].toString()), removeParent: mapValueOfType(json, r'removeParent'), stackParentId: mapValueOfType(json, r'stackParentId'), ); diff --git a/mobile/openapi/lib/model/asset_order.dart b/mobile/openapi/lib/model/asset_order.dart new file mode 100644 index 000000000..ddb68d381 --- /dev/null +++ b/mobile/openapi/lib/model/asset_order.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 AssetOrder { + /// Instantiate a new enum with the provided [value]. + const AssetOrder._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const asc = AssetOrder._(r'asc'); + static const desc = AssetOrder._(r'desc'); + + /// List of all possible values in this [enum][AssetOrder]. + static const values = [ + asc, + desc, + ]; + + static AssetOrder? fromJson(dynamic value) => AssetOrderTypeTransformer().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 = AssetOrder.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetOrder] to String, +/// and [decode] dynamic data back to [AssetOrder]. +class AssetOrderTypeTransformer { + factory AssetOrderTypeTransformer() => _instance ??= const AssetOrderTypeTransformer._(); + + const AssetOrderTypeTransformer._(); + + String encode(AssetOrder data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetOrder. + /// + /// 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. + AssetOrder? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'asc': return AssetOrder.asc; + case r'desc': return AssetOrder.desc; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetOrderTypeTransformer] instance. + static AssetOrderTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/cities_file.dart b/mobile/openapi/lib/model/cities_file.dart deleted file mode 100644 index 96f5d8e57..000000000 --- a/mobile/openapi/lib/model/cities_file.dart +++ /dev/null @@ -1,91 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.12 - -// 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 CitiesFile { - /// Instantiate a new enum with the provided [value]. - const CitiesFile._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const cities15000 = CitiesFile._(r'cities15000'); - static const cities5000 = CitiesFile._(r'cities5000'); - static const cities1000 = CitiesFile._(r'cities1000'); - static const cities500 = CitiesFile._(r'cities500'); - - /// List of all possible values in this [enum][CitiesFile]. - static const values = [ - cities15000, - cities5000, - cities1000, - cities500, - ]; - - static CitiesFile? fromJson(dynamic value) => CitiesFileTypeTransformer().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 = CitiesFile.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [CitiesFile] to String, -/// and [decode] dynamic data back to [CitiesFile]. -class CitiesFileTypeTransformer { - factory CitiesFileTypeTransformer() => _instance ??= const CitiesFileTypeTransformer._(); - - const CitiesFileTypeTransformer._(); - - String encode(CitiesFile data) => data.value; - - /// Decodes a [dynamic value][data] to a CitiesFile. - /// - /// 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. - CitiesFile? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'cities15000': return CitiesFile.cities15000; - case r'cities5000': return CitiesFile.cities5000; - case r'cities1000': return CitiesFile.cities1000; - case r'cities500': return CitiesFile.cities500; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [CitiesFileTypeTransformer] instance. - static CitiesFileTypeTransformer? _instance; -} - diff --git a/mobile/openapi/lib/model/create_user_dto.dart b/mobile/openapi/lib/model/create_user_dto.dart index 1345ac995..b33a3043c 100644 --- a/mobile/openapi/lib/model/create_user_dto.dart +++ b/mobile/openapi/lib/model/create_user_dto.dart @@ -15,9 +15,8 @@ class CreateUserDto { CreateUserDto({ required this.email, this.externalPath, - required this.firstName, - required this.lastName, this.memoriesEnabled, + required this.name, required this.password, this.storageLabel, }); @@ -26,10 +25,6 @@ class CreateUserDto { String? externalPath; - String firstName; - - String lastName; - /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -38,6 +33,8 @@ class CreateUserDto { /// bool? memoriesEnabled; + String name; + String password; String? storageLabel; @@ -46,9 +43,8 @@ class CreateUserDto { bool operator ==(Object other) => identical(this, other) || other is CreateUserDto && other.email == email && other.externalPath == externalPath && - other.firstName == firstName && - other.lastName == lastName && other.memoriesEnabled == memoriesEnabled && + other.name == name && other.password == password && other.storageLabel == storageLabel; @@ -57,14 +53,13 @@ class CreateUserDto { // ignore: unnecessary_parenthesis (email.hashCode) + (externalPath == null ? 0 : externalPath!.hashCode) + - (firstName.hashCode) + - (lastName.hashCode) + (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) + + (name.hashCode) + (password.hashCode) + (storageLabel == null ? 0 : storageLabel!.hashCode); @override - String toString() => 'CreateUserDto[email=$email, externalPath=$externalPath, firstName=$firstName, lastName=$lastName, memoriesEnabled=$memoriesEnabled, password=$password, storageLabel=$storageLabel]'; + String toString() => 'CreateUserDto[email=$email, externalPath=$externalPath, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, storageLabel=$storageLabel]'; Map toJson() { final json = {}; @@ -74,13 +69,12 @@ class CreateUserDto { } else { // json[r'externalPath'] = null; } - json[r'firstName'] = this.firstName; - json[r'lastName'] = this.lastName; if (this.memoriesEnabled != null) { json[r'memoriesEnabled'] = this.memoriesEnabled; } else { // json[r'memoriesEnabled'] = null; } + json[r'name'] = this.name; json[r'password'] = this.password; if (this.storageLabel != null) { json[r'storageLabel'] = this.storageLabel; @@ -100,9 +94,8 @@ class CreateUserDto { return CreateUserDto( email: mapValueOfType(json, r'email')!, externalPath: mapValueOfType(json, r'externalPath'), - firstName: mapValueOfType(json, r'firstName')!, - lastName: mapValueOfType(json, r'lastName')!, memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'), + name: mapValueOfType(json, r'name')!, password: mapValueOfType(json, r'password')!, storageLabel: mapValueOfType(json, r'storageLabel'), ); @@ -153,8 +146,7 @@ class CreateUserDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'email', - 'firstName', - 'lastName', + 'name', 'password', }; } diff --git a/mobile/openapi/lib/model/import_asset_dto.dart b/mobile/openapi/lib/model/import_asset_dto.dart deleted file mode 100644 index 7ba26da9d..000000000 --- a/mobile/openapi/lib/model/import_asset_dto.dart +++ /dev/null @@ -1,273 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.12 - -// 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 ImportAssetDto { - /// Returns a new [ImportAssetDto] instance. - ImportAssetDto({ - required this.assetPath, - required this.deviceAssetId, - required this.deviceId, - this.duration, - required this.fileCreatedAt, - required this.fileModifiedAt, - this.isArchived, - this.isExternal, - this.isFavorite, - this.isOffline, - this.isReadOnly = true, - this.isVisible, - this.libraryId, - this.sidecarPath, - }); - - String assetPath; - - String deviceAssetId; - - String deviceId; - - /// - /// Please note: 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? duration; - - DateTime fileCreatedAt; - - DateTime fileModifiedAt; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? isArchived; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? isExternal; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? isFavorite; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? isOffline; - - bool isReadOnly; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? isVisible; - - /// - /// Please note: 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? libraryId; - - /// - /// Please note: 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? sidecarPath; - - @override - bool operator ==(Object other) => identical(this, other) || other is ImportAssetDto && - other.assetPath == assetPath && - other.deviceAssetId == deviceAssetId && - other.deviceId == deviceId && - other.duration == duration && - other.fileCreatedAt == fileCreatedAt && - other.fileModifiedAt == fileModifiedAt && - other.isArchived == isArchived && - other.isExternal == isExternal && - other.isFavorite == isFavorite && - other.isOffline == isOffline && - other.isReadOnly == isReadOnly && - other.isVisible == isVisible && - other.libraryId == libraryId && - other.sidecarPath == sidecarPath; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (assetPath.hashCode) + - (deviceAssetId.hashCode) + - (deviceId.hashCode) + - (duration == null ? 0 : duration!.hashCode) + - (fileCreatedAt.hashCode) + - (fileModifiedAt.hashCode) + - (isArchived == null ? 0 : isArchived!.hashCode) + - (isExternal == null ? 0 : isExternal!.hashCode) + - (isFavorite == null ? 0 : isFavorite!.hashCode) + - (isOffline == null ? 0 : isOffline!.hashCode) + - (isReadOnly.hashCode) + - (isVisible == null ? 0 : isVisible!.hashCode) + - (libraryId == null ? 0 : libraryId!.hashCode) + - (sidecarPath == null ? 0 : sidecarPath!.hashCode); - - @override - String toString() => 'ImportAssetDto[assetPath=$assetPath, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, libraryId=$libraryId, sidecarPath=$sidecarPath]'; - - Map toJson() { - final json = {}; - json[r'assetPath'] = this.assetPath; - json[r'deviceAssetId'] = this.deviceAssetId; - json[r'deviceId'] = this.deviceId; - if (this.duration != null) { - json[r'duration'] = this.duration; - } else { - // json[r'duration'] = null; - } - json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String(); - json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String(); - if (this.isArchived != null) { - json[r'isArchived'] = this.isArchived; - } else { - // json[r'isArchived'] = null; - } - if (this.isExternal != null) { - json[r'isExternal'] = this.isExternal; - } else { - // json[r'isExternal'] = null; - } - if (this.isFavorite != null) { - json[r'isFavorite'] = this.isFavorite; - } else { - // json[r'isFavorite'] = null; - } - if (this.isOffline != null) { - json[r'isOffline'] = this.isOffline; - } else { - // json[r'isOffline'] = null; - } - json[r'isReadOnly'] = this.isReadOnly; - if (this.isVisible != null) { - json[r'isVisible'] = this.isVisible; - } else { - // json[r'isVisible'] = null; - } - if (this.libraryId != null) { - json[r'libraryId'] = this.libraryId; - } else { - // json[r'libraryId'] = null; - } - if (this.sidecarPath != null) { - json[r'sidecarPath'] = this.sidecarPath; - } else { - // json[r'sidecarPath'] = null; - } - return json; - } - - /// Returns a new [ImportAssetDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static ImportAssetDto? fromJson(dynamic value) { - if (value is Map) { - final json = value.cast(); - - return ImportAssetDto( - assetPath: mapValueOfType(json, r'assetPath')!, - deviceAssetId: mapValueOfType(json, r'deviceAssetId')!, - deviceId: mapValueOfType(json, r'deviceId')!, - duration: mapValueOfType(json, r'duration'), - fileCreatedAt: mapDateTime(json, r'fileCreatedAt', '')!, - fileModifiedAt: mapDateTime(json, r'fileModifiedAt', '')!, - isArchived: mapValueOfType(json, r'isArchived'), - isExternal: mapValueOfType(json, r'isExternal'), - isFavorite: mapValueOfType(json, r'isFavorite'), - isOffline: mapValueOfType(json, r'isOffline'), - isReadOnly: mapValueOfType(json, r'isReadOnly') ?? true, - isVisible: mapValueOfType(json, r'isVisible'), - libraryId: mapValueOfType(json, r'libraryId'), - sidecarPath: mapValueOfType(json, r'sidecarPath'), - ); - } - 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 = ImportAssetDto.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 = ImportAssetDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of ImportAssetDto-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] = ImportAssetDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'assetPath', - 'deviceAssetId', - 'deviceId', - 'fileCreatedAt', - 'fileModifiedAt', - }; -} - diff --git a/mobile/openapi/lib/model/login_response_dto.dart b/mobile/openapi/lib/model/login_response_dto.dart index 94da05a50..0293d04de 100644 --- a/mobile/openapi/lib/model/login_response_dto.dart +++ b/mobile/openapi/lib/model/login_response_dto.dart @@ -14,9 +14,8 @@ class LoginResponseDto { /// Returns a new [LoginResponseDto] instance. LoginResponseDto({ required this.accessToken, - required this.firstName, required this.isAdmin, - required this.lastName, + required this.name, required this.profileImagePath, required this.shouldChangePassword, required this.userEmail, @@ -25,11 +24,9 @@ class LoginResponseDto { String accessToken; - String firstName; - bool isAdmin; - String lastName; + String name; String profileImagePath; @@ -42,9 +39,8 @@ class LoginResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is LoginResponseDto && other.accessToken == accessToken && - other.firstName == firstName && other.isAdmin == isAdmin && - other.lastName == lastName && + other.name == name && other.profileImagePath == profileImagePath && other.shouldChangePassword == shouldChangePassword && other.userEmail == userEmail && @@ -54,23 +50,21 @@ class LoginResponseDto { int get hashCode => // ignore: unnecessary_parenthesis (accessToken.hashCode) + - (firstName.hashCode) + (isAdmin.hashCode) + - (lastName.hashCode) + + (name.hashCode) + (profileImagePath.hashCode) + (shouldChangePassword.hashCode) + (userEmail.hashCode) + (userId.hashCode); @override - String toString() => 'LoginResponseDto[accessToken=$accessToken, firstName=$firstName, isAdmin=$isAdmin, lastName=$lastName, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, userEmail=$userEmail, userId=$userId]'; + String toString() => 'LoginResponseDto[accessToken=$accessToken, isAdmin=$isAdmin, name=$name, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, userEmail=$userEmail, userId=$userId]'; Map toJson() { final json = {}; json[r'accessToken'] = this.accessToken; - json[r'firstName'] = this.firstName; json[r'isAdmin'] = this.isAdmin; - json[r'lastName'] = this.lastName; + json[r'name'] = this.name; json[r'profileImagePath'] = this.profileImagePath; json[r'shouldChangePassword'] = this.shouldChangePassword; json[r'userEmail'] = this.userEmail; @@ -87,9 +81,8 @@ class LoginResponseDto { return LoginResponseDto( accessToken: mapValueOfType(json, r'accessToken')!, - firstName: mapValueOfType(json, r'firstName')!, isAdmin: mapValueOfType(json, r'isAdmin')!, - lastName: mapValueOfType(json, r'lastName')!, + name: mapValueOfType(json, r'name')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, userEmail: mapValueOfType(json, r'userEmail')!, @@ -142,9 +135,8 @@ class LoginResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'accessToken', - 'firstName', 'isAdmin', - 'lastName', + 'name', 'profileImagePath', 'shouldChangePassword', 'userEmail', diff --git a/mobile/openapi/lib/model/map_theme.dart b/mobile/openapi/lib/model/map_theme.dart new file mode 100644 index 000000000..a67d9d2ef --- /dev/null +++ b/mobile/openapi/lib/model/map_theme.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 MapTheme { + /// Instantiate a new enum with the provided [value]. + const MapTheme._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const light = MapTheme._(r'light'); + static const dark = MapTheme._(r'dark'); + + /// List of all possible values in this [enum][MapTheme]. + static const values = [ + light, + dark, + ]; + + static MapTheme? fromJson(dynamic value) => MapThemeTypeTransformer().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 = MapTheme.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [MapTheme] to String, +/// and [decode] dynamic data back to [MapTheme]. +class MapThemeTypeTransformer { + factory MapThemeTypeTransformer() => _instance ??= const MapThemeTypeTransformer._(); + + const MapThemeTypeTransformer._(); + + String encode(MapTheme data) => data.value; + + /// Decodes a [dynamic value][data] to a MapTheme. + /// + /// 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. + MapTheme? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'light': return MapTheme.light; + case r'dark': return MapTheme.dark; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [MapThemeTypeTransformer] instance. + static MapThemeTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart new file mode 100644 index 000000000..6e1776b26 --- /dev/null +++ b/mobile/openapi/lib/model/partner_response_dto.dart @@ -0,0 +1,240 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 PartnerResponseDto { + /// Returns a new [PartnerResponseDto] instance. + PartnerResponseDto({ + required this.avatarColor, + required this.createdAt, + required this.deletedAt, + required this.email, + required this.externalPath, + required this.id, + this.inTimeline, + required this.isAdmin, + this.memoriesEnabled, + required this.name, + required this.oauthId, + required this.profileImagePath, + required this.shouldChangePassword, + required this.storageLabel, + required this.updatedAt, + }); + + UserAvatarColor avatarColor; + + DateTime createdAt; + + DateTime? deletedAt; + + String email; + + String? externalPath; + + String id; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? inTimeline; + + bool isAdmin; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? memoriesEnabled; + + String name; + + String oauthId; + + String profileImagePath; + + bool shouldChangePassword; + + String? storageLabel; + + DateTime updatedAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is PartnerResponseDto && + other.avatarColor == avatarColor && + other.createdAt == createdAt && + other.deletedAt == deletedAt && + other.email == email && + other.externalPath == externalPath && + other.id == id && + other.inTimeline == inTimeline && + other.isAdmin == isAdmin && + other.memoriesEnabled == memoriesEnabled && + other.name == name && + other.oauthId == oauthId && + other.profileImagePath == profileImagePath && + other.shouldChangePassword == shouldChangePassword && + other.storageLabel == storageLabel && + other.updatedAt == updatedAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (avatarColor.hashCode) + + (createdAt.hashCode) + + (deletedAt == null ? 0 : deletedAt!.hashCode) + + (email.hashCode) + + (externalPath == null ? 0 : externalPath!.hashCode) + + (id.hashCode) + + (inTimeline == null ? 0 : inTimeline!.hashCode) + + (isAdmin.hashCode) + + (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) + + (name.hashCode) + + (oauthId.hashCode) + + (profileImagePath.hashCode) + + (shouldChangePassword.hashCode) + + (storageLabel == null ? 0 : storageLabel!.hashCode) + + (updatedAt.hashCode); + + @override + String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]'; + + Map toJson() { + final json = {}; + json[r'avatarColor'] = this.avatarColor; + json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + if (this.deletedAt != null) { + json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + } else { + // json[r'deletedAt'] = null; + } + json[r'email'] = this.email; + if (this.externalPath != null) { + json[r'externalPath'] = this.externalPath; + } else { + // json[r'externalPath'] = null; + } + json[r'id'] = this.id; + if (this.inTimeline != null) { + json[r'inTimeline'] = this.inTimeline; + } else { + // json[r'inTimeline'] = null; + } + json[r'isAdmin'] = this.isAdmin; + if (this.memoriesEnabled != null) { + json[r'memoriesEnabled'] = this.memoriesEnabled; + } else { + // json[r'memoriesEnabled'] = null; + } + json[r'name'] = this.name; + json[r'oauthId'] = this.oauthId; + json[r'profileImagePath'] = this.profileImagePath; + json[r'shouldChangePassword'] = this.shouldChangePassword; + if (this.storageLabel != null) { + json[r'storageLabel'] = this.storageLabel; + } else { + // json[r'storageLabel'] = null; + } + json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + return json; + } + + /// Returns a new [PartnerResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PartnerResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return PartnerResponseDto( + avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!, + createdAt: mapDateTime(json, r'createdAt', '')!, + deletedAt: mapDateTime(json, r'deletedAt', ''), + email: mapValueOfType(json, r'email')!, + externalPath: mapValueOfType(json, r'externalPath'), + id: mapValueOfType(json, r'id')!, + inTimeline: mapValueOfType(json, r'inTimeline'), + isAdmin: mapValueOfType(json, r'isAdmin')!, + memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'), + name: mapValueOfType(json, r'name')!, + oauthId: mapValueOfType(json, r'oauthId')!, + profileImagePath: mapValueOfType(json, r'profileImagePath')!, + shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, + storageLabel: mapValueOfType(json, r'storageLabel'), + updatedAt: mapDateTime(json, r'updatedAt', '')!, + ); + } + 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 = PartnerResponseDto.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 = PartnerResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PartnerResponseDto-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] = PartnerResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'avatarColor', + 'createdAt', + 'deletedAt', + 'email', + 'externalPath', + 'id', + 'isAdmin', + 'name', + 'oauthId', + 'profileImagePath', + 'shouldChangePassword', + 'storageLabel', + 'updatedAt', + }; +} + diff --git a/mobile/openapi/lib/model/reaction_level.dart b/mobile/openapi/lib/model/reaction_level.dart new file mode 100644 index 000000000..5c8b8e62b --- /dev/null +++ b/mobile/openapi/lib/model/reaction_level.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 ReactionLevel { + /// Instantiate a new enum with the provided [value]. + const ReactionLevel._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const album = ReactionLevel._(r'album'); + static const asset = ReactionLevel._(r'asset'); + + /// List of all possible values in this [enum][ReactionLevel]. + static const values = [ + album, + asset, + ]; + + static ReactionLevel? fromJson(dynamic value) => ReactionLevelTypeTransformer().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 = ReactionLevel.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ReactionLevel] to String, +/// and [decode] dynamic data back to [ReactionLevel]. +class ReactionLevelTypeTransformer { + factory ReactionLevelTypeTransformer() => _instance ??= const ReactionLevelTypeTransformer._(); + + const ReactionLevelTypeTransformer._(); + + String encode(ReactionLevel data) => data.value; + + /// Decodes a [dynamic value][data] to a ReactionLevel. + /// + /// 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. + ReactionLevel? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'album': return ReactionLevel.album; + case r'asset': return ReactionLevel.asset; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ReactionLevelTypeTransformer] instance. + static ReactionLevelTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart index 25bdeb6d5..ffe5b2af3 100644 --- a/mobile/openapi/lib/model/server_config_dto.dart +++ b/mobile/openapi/lib/model/server_config_dto.dart @@ -15,7 +15,6 @@ class ServerConfigDto { ServerConfigDto({ required this.isInitialized, required this.loginPageMessage, - required this.mapTileUrl, required this.oauthButtonText, required this.trashDays, }); @@ -24,8 +23,6 @@ class ServerConfigDto { String loginPageMessage; - String mapTileUrl; - String oauthButtonText; int trashDays; @@ -34,7 +31,6 @@ class ServerConfigDto { bool operator ==(Object other) => identical(this, other) || other is ServerConfigDto && other.isInitialized == isInitialized && other.loginPageMessage == loginPageMessage && - other.mapTileUrl == mapTileUrl && other.oauthButtonText == oauthButtonText && other.trashDays == trashDays; @@ -43,18 +39,16 @@ class ServerConfigDto { // ignore: unnecessary_parenthesis (isInitialized.hashCode) + (loginPageMessage.hashCode) + - (mapTileUrl.hashCode) + (oauthButtonText.hashCode) + (trashDays.hashCode); @override - String toString() => 'ServerConfigDto[isInitialized=$isInitialized, loginPageMessage=$loginPageMessage, mapTileUrl=$mapTileUrl, oauthButtonText=$oauthButtonText, trashDays=$trashDays]'; + String toString() => 'ServerConfigDto[isInitialized=$isInitialized, loginPageMessage=$loginPageMessage, oauthButtonText=$oauthButtonText, trashDays=$trashDays]'; Map toJson() { final json = {}; json[r'isInitialized'] = this.isInitialized; json[r'loginPageMessage'] = this.loginPageMessage; - json[r'mapTileUrl'] = this.mapTileUrl; json[r'oauthButtonText'] = this.oauthButtonText; json[r'trashDays'] = this.trashDays; return json; @@ -70,7 +64,6 @@ class ServerConfigDto { return ServerConfigDto( isInitialized: mapValueOfType(json, r'isInitialized')!, loginPageMessage: mapValueOfType(json, r'loginPageMessage')!, - mapTileUrl: mapValueOfType(json, r'mapTileUrl')!, oauthButtonText: mapValueOfType(json, r'oauthButtonText')!, trashDays: mapValueOfType(json, r'trashDays')!, ); @@ -122,7 +115,6 @@ class ServerConfigDto { static const requiredKeys = { 'isInitialized', 'loginPageMessage', - 'mapTileUrl', 'oauthButtonText', 'trashDays', }; diff --git a/mobile/openapi/lib/model/sign_up_dto.dart b/mobile/openapi/lib/model/sign_up_dto.dart index e6b7f2abf..c6b4ee731 100644 --- a/mobile/openapi/lib/model/sign_up_dto.dart +++ b/mobile/openapi/lib/model/sign_up_dto.dart @@ -14,42 +14,36 @@ class SignUpDto { /// Returns a new [SignUpDto] instance. SignUpDto({ required this.email, - required this.firstName, - required this.lastName, + required this.name, required this.password, }); String email; - String firstName; - - String lastName; + String name; String password; @override bool operator ==(Object other) => identical(this, other) || other is SignUpDto && other.email == email && - other.firstName == firstName && - other.lastName == lastName && + other.name == name && other.password == password; @override int get hashCode => // ignore: unnecessary_parenthesis (email.hashCode) + - (firstName.hashCode) + - (lastName.hashCode) + + (name.hashCode) + (password.hashCode); @override - String toString() => 'SignUpDto[email=$email, firstName=$firstName, lastName=$lastName, password=$password]'; + String toString() => 'SignUpDto[email=$email, name=$name, password=$password]'; Map toJson() { final json = {}; json[r'email'] = this.email; - json[r'firstName'] = this.firstName; - json[r'lastName'] = this.lastName; + json[r'name'] = this.name; json[r'password'] = this.password; return json; } @@ -63,8 +57,7 @@ class SignUpDto { return SignUpDto( email: mapValueOfType(json, r'email')!, - firstName: mapValueOfType(json, r'firstName')!, - lastName: mapValueOfType(json, r'lastName')!, + name: mapValueOfType(json, r'name')!, password: mapValueOfType(json, r'password')!, ); } @@ -114,8 +107,7 @@ class SignUpDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'email', - 'firstName', - 'lastName', + 'name', 'password', }; } diff --git a/mobile/openapi/lib/model/system_config_map_dto.dart b/mobile/openapi/lib/model/system_config_map_dto.dart index 1d39f7152..667ae2115 100644 --- a/mobile/openapi/lib/model/system_config_map_dto.dart +++ b/mobile/openapi/lib/model/system_config_map_dto.dart @@ -13,32 +13,38 @@ part of openapi.api; class SystemConfigMapDto { /// Returns a new [SystemConfigMapDto] instance. SystemConfigMapDto({ + required this.darkStyle, required this.enabled, - required this.tileUrl, + required this.lightStyle, }); + String darkStyle; + bool enabled; - String tileUrl; + String lightStyle; @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigMapDto && + other.darkStyle == darkStyle && other.enabled == enabled && - other.tileUrl == tileUrl; + other.lightStyle == lightStyle; @override int get hashCode => // ignore: unnecessary_parenthesis + (darkStyle.hashCode) + (enabled.hashCode) + - (tileUrl.hashCode); + (lightStyle.hashCode); @override - String toString() => 'SystemConfigMapDto[enabled=$enabled, tileUrl=$tileUrl]'; + String toString() => 'SystemConfigMapDto[darkStyle=$darkStyle, enabled=$enabled, lightStyle=$lightStyle]'; Map toJson() { final json = {}; + json[r'darkStyle'] = this.darkStyle; json[r'enabled'] = this.enabled; - json[r'tileUrl'] = this.tileUrl; + json[r'lightStyle'] = this.lightStyle; return json; } @@ -50,8 +56,9 @@ class SystemConfigMapDto { final json = value.cast(); return SystemConfigMapDto( + darkStyle: mapValueOfType(json, r'darkStyle')!, enabled: mapValueOfType(json, r'enabled')!, - tileUrl: mapValueOfType(json, r'tileUrl')!, + lightStyle: mapValueOfType(json, r'lightStyle')!, ); } return null; @@ -99,8 +106,9 @@ class SystemConfigMapDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'darkStyle', 'enabled', - 'tileUrl', + 'lightStyle', }; } 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 727e5534f..d995d9667 100644 --- a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart +++ b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart @@ -13,31 +13,25 @@ part of openapi.api; class SystemConfigReverseGeocodingDto { /// Returns a new [SystemConfigReverseGeocodingDto] instance. SystemConfigReverseGeocodingDto({ - required this.citiesFileOverride, required this.enabled, }); - CitiesFile citiesFileOverride; - bool enabled; @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigReverseGeocodingDto && - other.citiesFileOverride == citiesFileOverride && other.enabled == enabled; @override int get hashCode => // ignore: unnecessary_parenthesis - (citiesFileOverride.hashCode) + (enabled.hashCode); @override - String toString() => 'SystemConfigReverseGeocodingDto[citiesFileOverride=$citiesFileOverride, enabled=$enabled]'; + String toString() => 'SystemConfigReverseGeocodingDto[enabled=$enabled]'; Map toJson() { final json = {}; - json[r'citiesFileOverride'] = this.citiesFileOverride; json[r'enabled'] = this.enabled; return json; } @@ -50,7 +44,6 @@ class SystemConfigReverseGeocodingDto { final json = value.cast(); return SystemConfigReverseGeocodingDto( - citiesFileOverride: CitiesFile.fromJson(json[r'citiesFileOverride'])!, enabled: mapValueOfType(json, r'enabled')!, ); } @@ -99,7 +92,6 @@ class SystemConfigReverseGeocodingDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'citiesFileOverride', 'enabled', }; } diff --git a/mobile/openapi/lib/model/update_album_dto.dart b/mobile/openapi/lib/model/update_album_dto.dart index 6c0bf3eca..32d4d2a60 100644 --- a/mobile/openapi/lib/model/update_album_dto.dart +++ b/mobile/openapi/lib/model/update_album_dto.dart @@ -16,6 +16,7 @@ class UpdateAlbumDto { this.albumName, this.albumThumbnailAssetId, this.description, + this.isActivityEnabled, }); /// @@ -42,21 +43,31 @@ class UpdateAlbumDto { /// String? 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 + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isActivityEnabled; + @override bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumDto && other.albumName == albumName && other.albumThumbnailAssetId == albumThumbnailAssetId && - other.description == description; + other.description == description && + other.isActivityEnabled == isActivityEnabled; @override int get hashCode => // ignore: unnecessary_parenthesis (albumName == null ? 0 : albumName!.hashCode) + (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) + - (description == null ? 0 : description!.hashCode); + (description == null ? 0 : description!.hashCode) + + (isActivityEnabled == null ? 0 : isActivityEnabled!.hashCode); @override - String toString() => 'UpdateAlbumDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, description=$description]'; + String toString() => 'UpdateAlbumDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, description=$description, isActivityEnabled=$isActivityEnabled]'; Map toJson() { final json = {}; @@ -75,6 +86,11 @@ class UpdateAlbumDto { } else { // json[r'description'] = null; } + if (this.isActivityEnabled != null) { + json[r'isActivityEnabled'] = this.isActivityEnabled; + } else { + // json[r'isActivityEnabled'] = null; + } return json; } @@ -89,6 +105,7 @@ class UpdateAlbumDto { albumName: mapValueOfType(json, r'albumName'), albumThumbnailAssetId: mapValueOfType(json, r'albumThumbnailAssetId'), description: mapValueOfType(json, r'description'), + isActivityEnabled: mapValueOfType(json, r'isActivityEnabled'), ); } return null; diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index d1f3570ef..d90b365b7 100644 --- a/mobile/openapi/lib/model/update_asset_dto.dart +++ b/mobile/openapi/lib/model/update_asset_dto.dart @@ -13,11 +13,22 @@ part of openapi.api; class UpdateAssetDto { /// Returns a new [UpdateAssetDto] instance. UpdateAssetDto({ + this.dateTimeOriginal, this.description, this.isArchived, this.isFavorite, + this.latitude, + this.longitude, }); + /// + /// Please note: 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? dateTimeOriginal; + /// /// Please note: 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,24 +53,51 @@ class UpdateAssetDto { /// bool? isFavorite; + /// + /// Please note: 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? latitude; + + /// + /// Please note: 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? longitude; + @override bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto && + other.dateTimeOriginal == dateTimeOriginal && other.description == description && other.isArchived == isArchived && - other.isFavorite == isFavorite; + other.isFavorite == isFavorite && + other.latitude == latitude && + other.longitude == longitude; @override int get hashCode => // ignore: unnecessary_parenthesis + (dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) + (description == null ? 0 : description!.hashCode) + (isArchived == null ? 0 : isArchived!.hashCode) + - (isFavorite == null ? 0 : isFavorite!.hashCode); + (isFavorite == null ? 0 : isFavorite!.hashCode) + + (latitude == null ? 0 : latitude!.hashCode) + + (longitude == null ? 0 : longitude!.hashCode); @override - String toString() => 'UpdateAssetDto[description=$description, isArchived=$isArchived, isFavorite=$isFavorite]'; + String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude]'; Map toJson() { final json = {}; + if (this.dateTimeOriginal != null) { + json[r'dateTimeOriginal'] = this.dateTimeOriginal; + } else { + // json[r'dateTimeOriginal'] = null; + } if (this.description != null) { json[r'description'] = this.description; } else { @@ -75,6 +113,16 @@ class UpdateAssetDto { } else { // json[r'isFavorite'] = null; } + if (this.latitude != null) { + json[r'latitude'] = this.latitude; + } else { + // json[r'latitude'] = null; + } + if (this.longitude != null) { + json[r'longitude'] = this.longitude; + } else { + // json[r'longitude'] = null; + } return json; } @@ -86,9 +134,16 @@ class UpdateAssetDto { final json = value.cast(); return UpdateAssetDto( + dateTimeOriginal: mapValueOfType(json, r'dateTimeOriginal'), description: mapValueOfType(json, r'description'), isArchived: mapValueOfType(json, r'isArchived'), isFavorite: mapValueOfType(json, r'isFavorite'), + latitude: json[r'latitude'] == null + ? null + : num.parse(json[r'latitude'].toString()), + longitude: json[r'longitude'] == null + ? null + : num.parse(json[r'longitude'].toString()), ); } return null; diff --git a/mobile/openapi/lib/model/search_asset_dto.dart b/mobile/openapi/lib/model/update_partner_dto.dart similarity index 54% rename from mobile/openapi/lib/model/search_asset_dto.dart rename to mobile/openapi/lib/model/update_partner_dto.dart index 02c744a06..6a8aaf161 100644 --- a/mobile/openapi/lib/model/search_asset_dto.dart +++ b/mobile/openapi/lib/model/update_partner_dto.dart @@ -10,51 +10,51 @@ part of openapi.api; -class SearchAssetDto { - /// Returns a new [SearchAssetDto] instance. - SearchAssetDto({ - required this.searchTerm, +class UpdatePartnerDto { + /// Returns a new [UpdatePartnerDto] instance. + UpdatePartnerDto({ + required this.inTimeline, }); - String searchTerm; + bool inTimeline; @override - bool operator ==(Object other) => identical(this, other) || other is SearchAssetDto && - other.searchTerm == searchTerm; + bool operator ==(Object other) => identical(this, other) || other is UpdatePartnerDto && + other.inTimeline == inTimeline; @override int get hashCode => // ignore: unnecessary_parenthesis - (searchTerm.hashCode); + (inTimeline.hashCode); @override - String toString() => 'SearchAssetDto[searchTerm=$searchTerm]'; + String toString() => 'UpdatePartnerDto[inTimeline=$inTimeline]'; Map toJson() { final json = {}; - json[r'searchTerm'] = this.searchTerm; + json[r'inTimeline'] = this.inTimeline; return json; } - /// Returns a new [SearchAssetDto] instance and imports its values from + /// Returns a new [UpdatePartnerDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static SearchAssetDto? fromJson(dynamic value) { + static UpdatePartnerDto? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return SearchAssetDto( - searchTerm: mapValueOfType(json, r'searchTerm')!, + return UpdatePartnerDto( + inTimeline: mapValueOfType(json, r'inTimeline')!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = SearchAssetDto.fromJson(row); + final value = UpdatePartnerDto.fromJson(row); if (value != null) { result.add(value); } @@ -63,12 +63,12 @@ class SearchAssetDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = SearchAssetDto.fromJson(entry.value); + final value = UpdatePartnerDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -77,14 +77,14 @@ class SearchAssetDto { return map; } - // maps a json object with a list of SearchAssetDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of UpdatePartnerDto-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] = SearchAssetDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = UpdatePartnerDto.listFromJson(entry.value, growable: growable,); } } return map; @@ -92,7 +92,7 @@ class SearchAssetDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'searchTerm', + 'inTimeline', }; } diff --git a/mobile/openapi/lib/model/update_user_dto.dart b/mobile/openapi/lib/model/update_user_dto.dart index 6202c6d1e..d0e46e7f5 100644 --- a/mobile/openapi/lib/model/update_user_dto.dart +++ b/mobile/openapi/lib/model/update_user_dto.dart @@ -13,18 +13,26 @@ part of openapi.api; class UpdateUserDto { /// Returns a new [UpdateUserDto] instance. UpdateUserDto({ + this.avatarColor, this.email, this.externalPath, - this.firstName, required this.id, this.isAdmin, - this.lastName, this.memoriesEnabled, + this.name, this.password, this.shouldChangePassword, this.storageLabel, }); + /// + /// Please note: 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. + /// + UserAvatarColor? avatarColor; + /// /// Please note: 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,14 +49,6 @@ class UpdateUserDto { /// String? externalPath; - /// - /// Please note: 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? firstName; - String id; /// @@ -65,7 +65,7 @@ class UpdateUserDto { /// source code must fall back to having a nullable type. /// Consider adding a "default:" property in the specification file to hide this note. /// - String? lastName; + bool? memoriesEnabled; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -73,7 +73,7 @@ class UpdateUserDto { /// source code must fall back to having a nullable type. /// Consider adding a "default:" property in the specification file to hide this note. /// - bool? memoriesEnabled; + String? name; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -101,13 +101,13 @@ class UpdateUserDto { @override bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto && + other.avatarColor == avatarColor && other.email == email && other.externalPath == externalPath && - other.firstName == firstName && other.id == id && other.isAdmin == isAdmin && - other.lastName == lastName && other.memoriesEnabled == memoriesEnabled && + other.name == name && other.password == password && other.shouldChangePassword == shouldChangePassword && other.storageLabel == storageLabel; @@ -115,22 +115,27 @@ class UpdateUserDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (avatarColor == null ? 0 : avatarColor!.hashCode) + (email == null ? 0 : email!.hashCode) + (externalPath == null ? 0 : externalPath!.hashCode) + - (firstName == null ? 0 : firstName!.hashCode) + (id.hashCode) + (isAdmin == null ? 0 : isAdmin!.hashCode) + - (lastName == null ? 0 : lastName!.hashCode) + (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) + + (name == null ? 0 : name!.hashCode) + (password == null ? 0 : password!.hashCode) + (shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode) + (storageLabel == null ? 0 : storageLabel!.hashCode); @override - String toString() => 'UpdateUserDto[email=$email, externalPath=$externalPath, firstName=$firstName, id=$id, isAdmin=$isAdmin, lastName=$lastName, memoriesEnabled=$memoriesEnabled, password=$password, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; + String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; Map toJson() { final json = {}; + if (this.avatarColor != null) { + json[r'avatarColor'] = this.avatarColor; + } else { + // json[r'avatarColor'] = null; + } if (this.email != null) { json[r'email'] = this.email; } else { @@ -140,11 +145,6 @@ class UpdateUserDto { json[r'externalPath'] = this.externalPath; } else { // json[r'externalPath'] = null; - } - if (this.firstName != null) { - json[r'firstName'] = this.firstName; - } else { - // json[r'firstName'] = null; } json[r'id'] = this.id; if (this.isAdmin != null) { @@ -152,16 +152,16 @@ class UpdateUserDto { } else { // json[r'isAdmin'] = null; } - if (this.lastName != null) { - json[r'lastName'] = this.lastName; - } else { - // json[r'lastName'] = null; - } if (this.memoriesEnabled != null) { json[r'memoriesEnabled'] = this.memoriesEnabled; } else { // json[r'memoriesEnabled'] = null; } + if (this.name != null) { + json[r'name'] = this.name; + } else { + // json[r'name'] = null; + } if (this.password != null) { json[r'password'] = this.password; } else { @@ -188,13 +188,13 @@ class UpdateUserDto { final json = value.cast(); return UpdateUserDto( + avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), email: mapValueOfType(json, r'email'), externalPath: mapValueOfType(json, r'externalPath'), - firstName: mapValueOfType(json, r'firstName'), id: mapValueOfType(json, r'id')!, isAdmin: mapValueOfType(json, r'isAdmin'), - lastName: mapValueOfType(json, r'lastName'), memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'), + name: mapValueOfType(json, r'name'), password: mapValueOfType(json, r'password'), shouldChangePassword: mapValueOfType(json, r'shouldChangePassword'), storageLabel: mapValueOfType(json, r'storageLabel'), diff --git a/mobile/openapi/lib/model/usage_by_user_dto.dart b/mobile/openapi/lib/model/usage_by_user_dto.dart index 052ebaa9f..27d8a2673 100644 --- a/mobile/openapi/lib/model/usage_by_user_dto.dart +++ b/mobile/openapi/lib/model/usage_by_user_dto.dart @@ -15,9 +15,8 @@ class UsageByUserDto { UsageByUserDto({ required this.photos, required this.usage, - required this.userFirstName, required this.userId, - required this.userLastName, + required this.userName, required this.videos, }); @@ -25,11 +24,9 @@ class UsageByUserDto { int usage; - String userFirstName; - String userId; - String userLastName; + String userName; int videos; @@ -37,9 +34,8 @@ class UsageByUserDto { bool operator ==(Object other) => identical(this, other) || other is UsageByUserDto && other.photos == photos && other.usage == usage && - other.userFirstName == userFirstName && other.userId == userId && - other.userLastName == userLastName && + other.userName == userName && other.videos == videos; @override @@ -47,21 +43,19 @@ class UsageByUserDto { // ignore: unnecessary_parenthesis (photos.hashCode) + (usage.hashCode) + - (userFirstName.hashCode) + (userId.hashCode) + - (userLastName.hashCode) + + (userName.hashCode) + (videos.hashCode); @override - String toString() => 'UsageByUserDto[photos=$photos, usage=$usage, userFirstName=$userFirstName, userId=$userId, userLastName=$userLastName, videos=$videos]'; + String toString() => 'UsageByUserDto[photos=$photos, usage=$usage, userId=$userId, userName=$userName, videos=$videos]'; Map toJson() { final json = {}; json[r'photos'] = this.photos; json[r'usage'] = this.usage; - json[r'userFirstName'] = this.userFirstName; json[r'userId'] = this.userId; - json[r'userLastName'] = this.userLastName; + json[r'userName'] = this.userName; json[r'videos'] = this.videos; return json; } @@ -76,9 +70,8 @@ class UsageByUserDto { return UsageByUserDto( photos: mapValueOfType(json, r'photos')!, usage: mapValueOfType(json, r'usage')!, - userFirstName: mapValueOfType(json, r'userFirstName')!, userId: mapValueOfType(json, r'userId')!, - userLastName: mapValueOfType(json, r'userLastName')!, + userName: mapValueOfType(json, r'userName')!, videos: mapValueOfType(json, r'videos')!, ); } @@ -129,9 +122,8 @@ class UsageByUserDto { static const requiredKeys = { 'photos', 'usage', - 'userFirstName', 'userId', - 'userLastName', + 'userName', 'videos', }; } diff --git a/mobile/openapi/lib/model/user_avatar_color.dart b/mobile/openapi/lib/model/user_avatar_color.dart new file mode 100644 index 000000000..075f58d3a --- /dev/null +++ b/mobile/openapi/lib/model/user_avatar_color.dart @@ -0,0 +1,109 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 UserAvatarColor { + /// Instantiate a new enum with the provided [value]. + const UserAvatarColor._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const primary = UserAvatarColor._(r'primary'); + static const pink = UserAvatarColor._(r'pink'); + static const red = UserAvatarColor._(r'red'); + static const yellow = UserAvatarColor._(r'yellow'); + static const blue = UserAvatarColor._(r'blue'); + static const green = UserAvatarColor._(r'green'); + static const purple = UserAvatarColor._(r'purple'); + static const orange = UserAvatarColor._(r'orange'); + static const gray = UserAvatarColor._(r'gray'); + static const amber = UserAvatarColor._(r'amber'); + + /// List of all possible values in this [enum][UserAvatarColor]. + static const values = [ + primary, + pink, + red, + yellow, + blue, + green, + purple, + orange, + gray, + amber, + ]; + + static UserAvatarColor? fromJson(dynamic value) => UserAvatarColorTypeTransformer().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 = UserAvatarColor.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [UserAvatarColor] to String, +/// and [decode] dynamic data back to [UserAvatarColor]. +class UserAvatarColorTypeTransformer { + factory UserAvatarColorTypeTransformer() => _instance ??= const UserAvatarColorTypeTransformer._(); + + const UserAvatarColorTypeTransformer._(); + + String encode(UserAvatarColor data) => data.value; + + /// Decodes a [dynamic value][data] to a UserAvatarColor. + /// + /// 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. + UserAvatarColor? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'primary': return UserAvatarColor.primary; + case r'pink': return UserAvatarColor.pink; + case r'red': return UserAvatarColor.red; + case r'yellow': return UserAvatarColor.yellow; + case r'blue': return UserAvatarColor.blue; + case r'green': return UserAvatarColor.green; + case r'purple': return UserAvatarColor.purple; + case r'orange': return UserAvatarColor.orange; + case r'gray': return UserAvatarColor.gray; + case r'amber': return UserAvatarColor.amber; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [UserAvatarColorTypeTransformer] instance. + static UserAvatarColorTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/user_dto.dart b/mobile/openapi/lib/model/user_dto.dart index e96588cf5..ad39f84e0 100644 --- a/mobile/openapi/lib/model/user_dto.dart +++ b/mobile/openapi/lib/model/user_dto.dart @@ -13,49 +13,49 @@ part of openapi.api; class UserDto { /// Returns a new [UserDto] instance. UserDto({ + required this.avatarColor, required this.email, - required this.firstName, required this.id, - required this.lastName, + required this.name, required this.profileImagePath, }); - String email; + UserAvatarColor avatarColor; - String firstName; + String email; String id; - String lastName; + String name; String profileImagePath; @override bool operator ==(Object other) => identical(this, other) || other is UserDto && + other.avatarColor == avatarColor && other.email == email && - other.firstName == firstName && other.id == id && - other.lastName == lastName && + other.name == name && other.profileImagePath == profileImagePath; @override int get hashCode => // ignore: unnecessary_parenthesis + (avatarColor.hashCode) + (email.hashCode) + - (firstName.hashCode) + (id.hashCode) + - (lastName.hashCode) + + (name.hashCode) + (profileImagePath.hashCode); @override - String toString() => 'UserDto[email=$email, firstName=$firstName, id=$id, lastName=$lastName, profileImagePath=$profileImagePath]'; + String toString() => 'UserDto[avatarColor=$avatarColor, email=$email, id=$id, name=$name, profileImagePath=$profileImagePath]'; Map toJson() { final json = {}; + json[r'avatarColor'] = this.avatarColor; json[r'email'] = this.email; - json[r'firstName'] = this.firstName; json[r'id'] = this.id; - json[r'lastName'] = this.lastName; + json[r'name'] = this.name; json[r'profileImagePath'] = this.profileImagePath; return json; } @@ -68,10 +68,10 @@ class UserDto { final json = value.cast(); return UserDto( + avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!, email: mapValueOfType(json, r'email')!, - firstName: mapValueOfType(json, r'firstName')!, id: mapValueOfType(json, r'id')!, - lastName: mapValueOfType(json, r'lastName')!, + name: mapValueOfType(json, r'name')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, ); } @@ -120,10 +120,10 @@ class UserDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'avatarColor', 'email', - 'firstName', 'id', - 'lastName', + 'name', 'profileImagePath', }; } diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 488c9aafd..11a182b6b 100644 --- a/mobile/openapi/lib/model/user_response_dto.dart +++ b/mobile/openapi/lib/model/user_response_dto.dart @@ -13,15 +13,15 @@ part of openapi.api; class UserResponseDto { /// Returns a new [UserResponseDto] instance. UserResponseDto({ + required this.avatarColor, required this.createdAt, required this.deletedAt, required this.email, required this.externalPath, - required this.firstName, required this.id, required this.isAdmin, - required this.lastName, this.memoriesEnabled, + required this.name, required this.oauthId, required this.profileImagePath, required this.shouldChangePassword, @@ -29,6 +29,8 @@ class UserResponseDto { required this.updatedAt, }); + UserAvatarColor avatarColor; + DateTime createdAt; DateTime? deletedAt; @@ -37,14 +39,10 @@ class UserResponseDto { String? externalPath; - String firstName; - String id; bool isAdmin; - String lastName; - /// /// Please note: 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,6 +51,8 @@ class UserResponseDto { /// bool? memoriesEnabled; + String name; + String oauthId; String profileImagePath; @@ -65,15 +65,15 @@ class UserResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is UserResponseDto && + other.avatarColor == avatarColor && other.createdAt == createdAt && other.deletedAt == deletedAt && other.email == email && other.externalPath == externalPath && - other.firstName == firstName && other.id == id && other.isAdmin == isAdmin && - other.lastName == lastName && other.memoriesEnabled == memoriesEnabled && + other.name == name && other.oauthId == oauthId && other.profileImagePath == profileImagePath && other.shouldChangePassword == shouldChangePassword && @@ -83,15 +83,15 @@ class UserResponseDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (avatarColor.hashCode) + (createdAt.hashCode) + (deletedAt == null ? 0 : deletedAt!.hashCode) + (email.hashCode) + (externalPath == null ? 0 : externalPath!.hashCode) + - (firstName.hashCode) + (id.hashCode) + (isAdmin.hashCode) + - (lastName.hashCode) + (memoriesEnabled == null ? 0 : memoriesEnabled!.hashCode) + + (name.hashCode) + (oauthId.hashCode) + (profileImagePath.hashCode) + (shouldChangePassword.hashCode) + @@ -99,10 +99,11 @@ class UserResponseDto { (updatedAt.hashCode); @override - String toString() => 'UserResponseDto[createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, firstName=$firstName, id=$id, isAdmin=$isAdmin, lastName=$lastName, memoriesEnabled=$memoriesEnabled, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]'; + String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]'; Map toJson() { final json = {}; + json[r'avatarColor'] = this.avatarColor; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); if (this.deletedAt != null) { json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); @@ -115,15 +116,14 @@ class UserResponseDto { } else { // json[r'externalPath'] = null; } - json[r'firstName'] = this.firstName; json[r'id'] = this.id; json[r'isAdmin'] = this.isAdmin; - json[r'lastName'] = this.lastName; if (this.memoriesEnabled != null) { json[r'memoriesEnabled'] = this.memoriesEnabled; } else { // json[r'memoriesEnabled'] = null; } + json[r'name'] = this.name; json[r'oauthId'] = this.oauthId; json[r'profileImagePath'] = this.profileImagePath; json[r'shouldChangePassword'] = this.shouldChangePassword; @@ -144,15 +144,15 @@ class UserResponseDto { final json = value.cast(); return UserResponseDto( + avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!, createdAt: mapDateTime(json, r'createdAt', '')!, deletedAt: mapDateTime(json, r'deletedAt', ''), email: mapValueOfType(json, r'email')!, externalPath: mapValueOfType(json, r'externalPath'), - firstName: mapValueOfType(json, r'firstName')!, id: mapValueOfType(json, r'id')!, isAdmin: mapValueOfType(json, r'isAdmin')!, - lastName: mapValueOfType(json, r'lastName')!, memoriesEnabled: mapValueOfType(json, r'memoriesEnabled'), + name: mapValueOfType(json, r'name')!, oauthId: mapValueOfType(json, r'oauthId')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, @@ -205,14 +205,14 @@ class UserResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'avatarColor', 'createdAt', 'deletedAt', 'email', 'externalPath', - 'firstName', 'id', 'isAdmin', - 'lastName', + 'name', 'oauthId', 'profileImagePath', 'shouldChangePassword', diff --git a/mobile/openapi/test/activity_api_test.dart b/mobile/openapi/test/activity_api_test.dart index 9b6fe1a6c..7353c6ea3 100644 --- a/mobile/openapi/test/activity_api_test.dart +++ b/mobile/openapi/test/activity_api_test.dart @@ -27,7 +27,7 @@ void main() { // TODO }); - //Future> getActivities(String albumId, { String assetId, ReactionType type, String userId }) async + //Future> getActivities(String albumId, { String assetId, ReactionType type, ReactionLevel level, String userId }) async test('test getActivities', () async { // TODO }); diff --git a/mobile/openapi/test/admin_signup_response_dto_test.dart b/mobile/openapi/test/admin_signup_response_dto_test.dart deleted file mode 100644 index 62582e9b3..000000000 --- a/mobile/openapi/test/admin_signup_response_dto_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.12 - -// 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 - -import 'package:openapi/api.dart'; -import 'package:test/test.dart'; - -// tests for AdminSignupResponseDto -void main() { - // final instance = AdminSignupResponseDto(); - - group('test AdminSignupResponseDto', () { - // DateTime createdAt - test('to test the property `createdAt`', () async { - // TODO - }); - - // String email - test('to test the property `email`', () async { - // TODO - }); - - // String firstName - test('to test the property `firstName`', () async { - // TODO - }); - - // String id - test('to test the property `id`', () async { - // TODO - }); - - // String lastName - test('to test the property `lastName`', () async { - // TODO - }); - - - }); - -} diff --git a/mobile/openapi/test/album_response_dto_test.dart b/mobile/openapi/test/album_response_dto_test.dart index c84174200..933f77c19 100644 --- a/mobile/openapi/test/album_response_dto_test.dart +++ b/mobile/openapi/test/album_response_dto_test.dart @@ -61,6 +61,11 @@ void main() { // TODO }); + // bool isActivityEnabled + test('to test the property `isActivityEnabled`', () async { + // TODO + }); + // DateTime lastModifiedAssetTimestamp test('to test the property `lastModifiedAssetTimestamp`', () async { // TODO diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index e241c34ca..c4f6c8511 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -58,6 +58,13 @@ void main() { // TODO }); + // Get all asset of a device that are in the database, ID only. + // + //Future> getAllUserAssetsByDeviceId(String deviceId) async + test('test getAllUserAssetsByDeviceId', () async { + // TODO + }); + // Get a single asset's information // //Future getAssetById(String id, { String key }) async @@ -110,28 +117,23 @@ void main() { // TODO }); - //Future> getTimeBucket(TimeBucketSize size, String timeBucket, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, bool isTrashed, bool withStacked, String key }) async + //Future> getTimeBucket(TimeBucketSize size, String timeBucket, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, bool isTrashed, bool withStacked, bool withPartners, String key }) async test('test getTimeBucket', () async { // TODO }); - //Future> getTimeBuckets(TimeBucketSize size, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, bool isTrashed, bool withStacked, String key }) async + //Future> getTimeBuckets(TimeBucketSize size, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, bool isTrashed, bool withStacked, bool withPartners, String key }) async test('test getTimeBuckets', () async { // TODO }); - // Get all asset of a device that are in the database, ID only. + // Use /asset/device/:deviceId instead - Remove in 1.92 release // //Future> getUserAssetsByDeviceId(String deviceId) async test('test getUserAssetsByDeviceId', () async { // TODO }); - //Future importFile(ImportAssetDto importAssetDto) async - test('test importFile', () async { - // TODO - }); - //Future restoreAssets(BulkIdsDto bulkIdsDto) async test('test restoreAssets', () async { // TODO @@ -147,8 +149,8 @@ void main() { // TODO }); - //Future> searchAsset(SearchAssetDto searchAssetDto) async - test('test searchAsset', () async { + //Future> searchAssets({ String id, String libraryId, AssetTypeEnum type, AssetOrder order, String deviceAssetId, String deviceId, String checksum, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isOffline, bool isReadOnly, bool isVisible, bool withDeleted, bool withStacked, bool withExif, bool withPeople, DateTime createdBefore, DateTime createdAfter, DateTime updatedBefore, DateTime updatedAfter, DateTime trashedBefore, DateTime trashedAfter, DateTime takenBefore, DateTime takenAfter, String originalFileName, String originalPath, String resizePath, String webpPath, String encodedVideoPath, String city, String state, String country, String make, String model, String lensModel, num page, num size }) async + test('test searchAssets', () async { // TODO }); diff --git a/mobile/openapi/test/asset_bulk_update_dto_test.dart b/mobile/openapi/test/asset_bulk_update_dto_test.dart index 06f65de66..d04bdd809 100644 --- a/mobile/openapi/test/asset_bulk_update_dto_test.dart +++ b/mobile/openapi/test/asset_bulk_update_dto_test.dart @@ -16,6 +16,11 @@ void main() { // final instance = AssetBulkUpdateDto(); group('test AssetBulkUpdateDto', () { + // String dateTimeOriginal + test('to test the property `dateTimeOriginal`', () async { + // TODO + }); + // List ids (default value: const []) test('to test the property `ids`', () async { // TODO @@ -31,6 +36,16 @@ void main() { // TODO }); + // num latitude + test('to test the property `latitude`', () async { + // TODO + }); + + // num longitude + test('to test the property `longitude`', () async { + // TODO + }); + // bool removeParent test('to test the property `removeParent`', () async { // TODO diff --git a/mobile/openapi/test/cities_file_test.dart b/mobile/openapi/test/asset_order_test.dart similarity index 86% rename from mobile/openapi/test/cities_file_test.dart rename to mobile/openapi/test/asset_order_test.dart index cfe63b754..4a1490810 100644 --- a/mobile/openapi/test/cities_file_test.dart +++ b/mobile/openapi/test/asset_order_test.dart @@ -11,10 +11,10 @@ import 'package:openapi/api.dart'; import 'package:test/test.dart'; -// tests for CitiesFile +// tests for AssetOrder void main() { - group('test CitiesFile', () { + group('test AssetOrder', () { }); diff --git a/mobile/openapi/test/authentication_api_test.dart b/mobile/openapi/test/authentication_api_test.dart index 22eb7550f..aa2f1879d 100644 --- a/mobile/openapi/test/authentication_api_test.dart +++ b/mobile/openapi/test/authentication_api_test.dart @@ -47,7 +47,7 @@ void main() { // TODO }); - //Future signUpAdmin(SignUpDto signUpDto) async + //Future signUpAdmin(SignUpDto signUpDto) async test('test signUpAdmin', () async { // TODO }); diff --git a/mobile/openapi/test/create_user_dto_test.dart b/mobile/openapi/test/create_user_dto_test.dart index 9ce64c1e0..0820ef0ed 100644 --- a/mobile/openapi/test/create_user_dto_test.dart +++ b/mobile/openapi/test/create_user_dto_test.dart @@ -26,21 +26,16 @@ void main() { // TODO }); - // String firstName - test('to test the property `firstName`', () async { - // TODO - }); - - // String lastName - test('to test the property `lastName`', () async { - // TODO - }); - // bool memoriesEnabled test('to test the property `memoriesEnabled`', () async { // TODO }); + // String name + test('to test the property `name`', () async { + // TODO + }); + // String password test('to test the property `password`', () async { // TODO diff --git a/mobile/openapi/test/import_asset_dto_test.dart b/mobile/openapi/test/import_asset_dto_test.dart deleted file mode 100644 index 94e2a3bc4..000000000 --- a/mobile/openapi/test/import_asset_dto_test.dart +++ /dev/null @@ -1,92 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.12 - -// 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 - -import 'package:openapi/api.dart'; -import 'package:test/test.dart'; - -// tests for ImportAssetDto -void main() { - // final instance = ImportAssetDto(); - - group('test ImportAssetDto', () { - // String assetPath - test('to test the property `assetPath`', () async { - // TODO - }); - - // String deviceAssetId - test('to test the property `deviceAssetId`', () async { - // TODO - }); - - // String deviceId - test('to test the property `deviceId`', () async { - // TODO - }); - - // String duration - test('to test the property `duration`', () async { - // TODO - }); - - // DateTime fileCreatedAt - test('to test the property `fileCreatedAt`', () async { - // TODO - }); - - // DateTime fileModifiedAt - test('to test the property `fileModifiedAt`', () async { - // TODO - }); - - // bool isArchived - test('to test the property `isArchived`', () async { - // TODO - }); - - // bool isExternal - test('to test the property `isExternal`', () async { - // TODO - }); - - // bool isFavorite - test('to test the property `isFavorite`', () async { - // TODO - }); - - // bool isOffline - test('to test the property `isOffline`', () async { - // TODO - }); - - // bool isReadOnly (default value: true) - test('to test the property `isReadOnly`', () async { - // TODO - }); - - // bool isVisible - test('to test the property `isVisible`', () async { - // TODO - }); - - // String libraryId - test('to test the property `libraryId`', () async { - // TODO - }); - - // String sidecarPath - test('to test the property `sidecarPath`', () async { - // TODO - }); - - - }); - -} diff --git a/mobile/openapi/test/login_response_dto_test.dart b/mobile/openapi/test/login_response_dto_test.dart index 408b7fade..a8365ff06 100644 --- a/mobile/openapi/test/login_response_dto_test.dart +++ b/mobile/openapi/test/login_response_dto_test.dart @@ -21,18 +21,13 @@ void main() { // TODO }); - // String firstName - test('to test the property `firstName`', () async { - // TODO - }); - // bool isAdmin test('to test the property `isAdmin`', () async { // TODO }); - // String lastName - test('to test the property `lastName`', () async { + // String name + test('to test the property `name`', () async { // TODO }); diff --git a/mobile/openapi/test/map_theme_test.dart b/mobile/openapi/test/map_theme_test.dart new file mode 100644 index 000000000..82fa9ff3d --- /dev/null +++ b/mobile/openapi/test/map_theme_test.dart @@ -0,0 +1,21 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for MapTheme +void main() { + + group('test MapTheme', () { + + }); + +} diff --git a/mobile/openapi/test/partner_api_test.dart b/mobile/openapi/test/partner_api_test.dart index fa5a59d2a..daaf65644 100644 --- a/mobile/openapi/test/partner_api_test.dart +++ b/mobile/openapi/test/partner_api_test.dart @@ -17,12 +17,12 @@ void main() { // final instance = PartnerApi(); group('tests for PartnerApi', () { - //Future createPartner(String id) async + //Future createPartner(String id) async test('test createPartner', () async { // TODO }); - //Future> getPartners(String direction) async + //Future> getPartners(String direction) async test('test getPartners', () async { // TODO }); @@ -32,5 +32,10 @@ void main() { // TODO }); + //Future updatePartner(String id, UpdatePartnerDto updatePartnerDto) async + test('test updatePartner', () async { + // TODO + }); + }); } diff --git a/mobile/openapi/test/partner_response_dto_test.dart b/mobile/openapi/test/partner_response_dto_test.dart new file mode 100644 index 000000000..50ac1d805 --- /dev/null +++ b/mobile/openapi/test/partner_response_dto_test.dart @@ -0,0 +1,97 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for PartnerResponseDto +void main() { + // final instance = PartnerResponseDto(); + + group('test PartnerResponseDto', () { + // UserAvatarColor avatarColor + test('to test the property `avatarColor`', () async { + // TODO + }); + + // DateTime createdAt + test('to test the property `createdAt`', () async { + // TODO + }); + + // DateTime deletedAt + test('to test the property `deletedAt`', () async { + // TODO + }); + + // String email + test('to test the property `email`', () async { + // TODO + }); + + // String externalPath + test('to test the property `externalPath`', () async { + // TODO + }); + + // String id + test('to test the property `id`', () async { + // TODO + }); + + // bool inTimeline + test('to test the property `inTimeline`', () async { + // TODO + }); + + // bool isAdmin + test('to test the property `isAdmin`', () async { + // TODO + }); + + // bool memoriesEnabled + test('to test the property `memoriesEnabled`', () async { + // TODO + }); + + // String name + test('to test the property `name`', () async { + // TODO + }); + + // String oauthId + test('to test the property `oauthId`', () async { + // TODO + }); + + // String profileImagePath + test('to test the property `profileImagePath`', () async { + // TODO + }); + + // bool shouldChangePassword + test('to test the property `shouldChangePassword`', () async { + // TODO + }); + + // String storageLabel + test('to test the property `storageLabel`', () async { + // TODO + }); + + // DateTime updatedAt + test('to test the property `updatedAt`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/reaction_level_test.dart b/mobile/openapi/test/reaction_level_test.dart new file mode 100644 index 000000000..6fcba58b1 --- /dev/null +++ b/mobile/openapi/test/reaction_level_test.dart @@ -0,0 +1,21 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for ReactionLevel +void main() { + + group('test ReactionLevel', () { + + }); + +} diff --git a/mobile/openapi/test/server_config_dto_test.dart b/mobile/openapi/test/server_config_dto_test.dart index 0aa581b13..d43e47823 100644 --- a/mobile/openapi/test/server_config_dto_test.dart +++ b/mobile/openapi/test/server_config_dto_test.dart @@ -26,11 +26,6 @@ void main() { // TODO }); - // String mapTileUrl - test('to test the property `mapTileUrl`', () async { - // TODO - }); - // String oauthButtonText test('to test the property `oauthButtonText`', () async { // TODO diff --git a/mobile/openapi/test/sign_up_dto_test.dart b/mobile/openapi/test/sign_up_dto_test.dart index 5b02f0435..3c255f726 100644 --- a/mobile/openapi/test/sign_up_dto_test.dart +++ b/mobile/openapi/test/sign_up_dto_test.dart @@ -21,13 +21,8 @@ void main() { // TODO }); - // String firstName - test('to test the property `firstName`', () async { - // TODO - }); - - // String lastName - test('to test the property `lastName`', () async { + // String name + test('to test the property `name`', () async { // TODO }); diff --git a/mobile/openapi/test/system_config_api_test.dart b/mobile/openapi/test/system_config_api_test.dart index b8e57aaae..fcf1ecfc5 100644 --- a/mobile/openapi/test/system_config_api_test.dart +++ b/mobile/openapi/test/system_config_api_test.dart @@ -27,6 +27,11 @@ void main() { // TODO }); + //Future getMapStyle(MapTheme theme) async + test('test getMapStyle', () async { + // TODO + }); + //Future getStorageTemplateOptions() async test('test getStorageTemplateOptions', () async { // TODO diff --git a/mobile/openapi/test/system_config_map_dto_test.dart b/mobile/openapi/test/system_config_map_dto_test.dart index d0b753dc0..b42753d4f 100644 --- a/mobile/openapi/test/system_config_map_dto_test.dart +++ b/mobile/openapi/test/system_config_map_dto_test.dart @@ -16,13 +16,18 @@ void main() { // final instance = SystemConfigMapDto(); group('test SystemConfigMapDto', () { + // String darkStyle + test('to test the property `darkStyle`', () async { + // TODO + }); + // bool enabled test('to test the property `enabled`', () async { // TODO }); - // String tileUrl - test('to test the property `tileUrl`', () async { + // String lightStyle + test('to test the property `lightStyle`', () async { // TODO }); diff --git a/mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart b/mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart index 12f7655ea..b4aa477df 100644 --- a/mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart +++ b/mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart @@ -16,11 +16,6 @@ void main() { // final instance = SystemConfigReverseGeocodingDto(); group('test SystemConfigReverseGeocodingDto', () { - // CitiesFile citiesFileOverride - test('to test the property `citiesFileOverride`', () async { - // TODO - }); - // bool enabled test('to test the property `enabled`', () async { // TODO diff --git a/mobile/openapi/test/update_album_dto_test.dart b/mobile/openapi/test/update_album_dto_test.dart index 7b8472ad3..67ec80010 100644 --- a/mobile/openapi/test/update_album_dto_test.dart +++ b/mobile/openapi/test/update_album_dto_test.dart @@ -31,6 +31,11 @@ void main() { // TODO }); + // bool isActivityEnabled + test('to test the property `isActivityEnabled`', () async { + // TODO + }); + }); diff --git a/mobile/openapi/test/update_asset_dto_test.dart b/mobile/openapi/test/update_asset_dto_test.dart index b2966e961..9d9874beb 100644 --- a/mobile/openapi/test/update_asset_dto_test.dart +++ b/mobile/openapi/test/update_asset_dto_test.dart @@ -16,6 +16,11 @@ void main() { // final instance = UpdateAssetDto(); group('test UpdateAssetDto', () { + // String dateTimeOriginal + test('to test the property `dateTimeOriginal`', () async { + // TODO + }); + // String description test('to test the property `description`', () async { // TODO @@ -31,6 +36,16 @@ void main() { // TODO }); + // num latitude + test('to test the property `latitude`', () async { + // TODO + }); + + // num longitude + test('to test the property `longitude`', () async { + // TODO + }); + }); diff --git a/mobile/openapi/test/search_asset_dto_test.dart b/mobile/openapi/test/update_partner_dto_test.dart similarity index 66% rename from mobile/openapi/test/search_asset_dto_test.dart rename to mobile/openapi/test/update_partner_dto_test.dart index 60021265c..ca569914b 100644 --- a/mobile/openapi/test/search_asset_dto_test.dart +++ b/mobile/openapi/test/update_partner_dto_test.dart @@ -11,13 +11,13 @@ import 'package:openapi/api.dart'; import 'package:test/test.dart'; -// tests for SearchAssetDto +// tests for UpdatePartnerDto void main() { - // final instance = SearchAssetDto(); + // final instance = UpdatePartnerDto(); - group('test SearchAssetDto', () { - // String searchTerm - test('to test the property `searchTerm`', () async { + group('test UpdatePartnerDto', () { + // bool inTimeline + test('to test the property `inTimeline`', () async { // TODO }); diff --git a/mobile/openapi/test/update_user_dto_test.dart b/mobile/openapi/test/update_user_dto_test.dart index 511de33af..0b4cc0b65 100644 --- a/mobile/openapi/test/update_user_dto_test.dart +++ b/mobile/openapi/test/update_user_dto_test.dart @@ -16,6 +16,11 @@ void main() { // final instance = UpdateUserDto(); group('test UpdateUserDto', () { + // UserAvatarColor avatarColor + test('to test the property `avatarColor`', () async { + // TODO + }); + // String email test('to test the property `email`', () async { // TODO @@ -26,11 +31,6 @@ void main() { // TODO }); - // String firstName - test('to test the property `firstName`', () async { - // TODO - }); - // String id test('to test the property `id`', () async { // TODO @@ -41,13 +41,13 @@ void main() { // TODO }); - // String lastName - test('to test the property `lastName`', () async { + // bool memoriesEnabled + test('to test the property `memoriesEnabled`', () async { // TODO }); - // bool memoriesEnabled - test('to test the property `memoriesEnabled`', () async { + // String name + test('to test the property `name`', () async { // TODO }); diff --git a/mobile/openapi/test/usage_by_user_dto_test.dart b/mobile/openapi/test/usage_by_user_dto_test.dart index 685d49e9f..51becb06f 100644 --- a/mobile/openapi/test/usage_by_user_dto_test.dart +++ b/mobile/openapi/test/usage_by_user_dto_test.dart @@ -26,18 +26,13 @@ void main() { // TODO }); - // String userFirstName - test('to test the property `userFirstName`', () async { - // TODO - }); - // String userId test('to test the property `userId`', () async { // TODO }); - // String userLastName - test('to test the property `userLastName`', () async { + // String userName + test('to test the property `userName`', () async { // TODO }); diff --git a/mobile/openapi/test/user_api_test.dart b/mobile/openapi/test/user_api_test.dart index 86c33c7e0..26ebf3d7e 100644 --- a/mobile/openapi/test/user_api_test.dart +++ b/mobile/openapi/test/user_api_test.dart @@ -27,6 +27,11 @@ void main() { // TODO }); + //Future deleteProfileImage() async + test('test deleteProfileImage', () async { + // TODO + }); + //Future deleteUser(String id) async test('test deleteUser', () async { // TODO diff --git a/mobile/openapi/test/user_avatar_color_test.dart b/mobile/openapi/test/user_avatar_color_test.dart new file mode 100644 index 000000000..83480b580 --- /dev/null +++ b/mobile/openapi/test/user_avatar_color_test.dart @@ -0,0 +1,21 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for UserAvatarColor +void main() { + + group('test UserAvatarColor', () { + + }); + +} diff --git a/mobile/openapi/test/user_dto_test.dart b/mobile/openapi/test/user_dto_test.dart index fa1b7da86..20229ff65 100644 --- a/mobile/openapi/test/user_dto_test.dart +++ b/mobile/openapi/test/user_dto_test.dart @@ -16,13 +16,13 @@ void main() { // final instance = UserDto(); group('test UserDto', () { - // String email - test('to test the property `email`', () async { + // UserAvatarColor avatarColor + test('to test the property `avatarColor`', () async { // TODO }); - // String firstName - test('to test the property `firstName`', () async { + // String email + test('to test the property `email`', () async { // TODO }); @@ -31,8 +31,8 @@ void main() { // TODO }); - // String lastName - test('to test the property `lastName`', () async { + // String name + test('to test the property `name`', () async { // TODO }); diff --git a/mobile/openapi/test/user_response_dto_test.dart b/mobile/openapi/test/user_response_dto_test.dart index 9e2095607..aa0717e74 100644 --- a/mobile/openapi/test/user_response_dto_test.dart +++ b/mobile/openapi/test/user_response_dto_test.dart @@ -16,6 +16,11 @@ void main() { // final instance = UserResponseDto(); group('test UserResponseDto', () { + // UserAvatarColor avatarColor + test('to test the property `avatarColor`', () async { + // TODO + }); + // DateTime createdAt test('to test the property `createdAt`', () async { // TODO @@ -36,11 +41,6 @@ void main() { // TODO }); - // String firstName - test('to test the property `firstName`', () async { - // TODO - }); - // String id test('to test the property `id`', () async { // TODO @@ -51,13 +51,13 @@ void main() { // TODO }); - // String lastName - test('to test the property `lastName`', () async { + // bool memoriesEnabled + test('to test the property `memoriesEnabled`', () async { // TODO }); - // bool memoriesEnabled - test('to test the property `memoriesEnabled`', () async { + // String name + test('to test the property `name`', () async { // TODO }); diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index da1f6f847..df79d5afe 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + url: "https://pub.dev" + source: hosted + version: "0.11.2" archive: dependency: transitive description: @@ -201,6 +209,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.7.0" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" clock: dependency: transitive description: @@ -281,6 +305,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + custom_lint: + dependency: "direct dev" + description: + name: custom_lint + sha256: f9a828b696930cf8307f9a3617b2b65c9b370e484dc845d69100cadb77506778 + url: "https://pub.dev" + source: hosted + version: "0.5.6" + custom_lint_builder: + dependency: transitive + description: + name: custom_lint_builder + sha256: c6f656a4d83385fc0656ae60410ed06bb382898c45627bfb8bbaa323aea97883 + url: "https://pub.dev" + source: hosted + version: "0.5.6" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: e20a67737adcf0cf2465e734dd624af535add11f9edd1f2d444909b5b0749650 + url: "https://pub.dev" + source: hosted + version: "0.5.6" dart_style: dependency: transitive description: @@ -345,6 +393,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" + executor_lib: + dependency: transitive + description: + name: executor_lib + sha256: "544889daa5726462657dab6410b75f2f8e3a77479d85b307a25c346e243bc38e" + url: "https://pub.dev" + source: hosted + version: "1.1.1" fake_async: dependency: transitive description: @@ -447,10 +503,10 @@ packages: dependency: "direct main" description: name: flutter_hooks - sha256: "6a126f703b89499818d73305e4ce1e3de33b4ae1c5512e3b8eab4b986f46774c" + sha256: "7c8db779c2d1010aa7f9ea3fbefe8f86524fcb87b69e8b0af31e1a4b55422dec" url: "https://pub.dev" source: hosted - version: "0.18.6" + version: "0.20.3" flutter_launcher_icons: dependency: "direct dev" description: @@ -532,10 +588,10 @@ packages: dependency: transitive description: name: flutter_riverpod - sha256: b6cb0041c6c11cefb2dcb97ef436eba43c6d41287ac6d8ca93e02a497f53a4f3 + sha256: "305203d1578f6857675f9730568548b03900ce53afd319f4aa9d2fa943334dbe" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.4.5" flutter_test: dependency: "direct dev" description: flutter @@ -578,6 +634,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" frontend_server_client: dependency: transitive description: @@ -659,10 +723,18 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: "2bb8ae6a729e1334f71f1ef68dd5f0400dca8f01de8cbdcde062584a68017b18" + sha256: "2827136ecc0c2abffc3a58e575db6d5b84d159977fa1edc223c97bf566aa8c73" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "2.4.5" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: "94ee21a60ea2836500799f3af035dc3212b1562027f1e0031c14e087f0231449" + url: "https://pub.dev" + source: hosted + version: "4.1.0" html: dependency: transitive description: @@ -1147,6 +1219,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08" + url: "https://pub.dev" + source: hosted + version: "2.1.0" provider: dependency: transitive description: @@ -1175,10 +1255,42 @@ packages: dependency: transitive description: name: riverpod - sha256: b0657b5b30c81a3184bdaab353045f0a403ebd60bb381591a8b7ad77dcade793 + sha256: "2e84315036e64c59affaff7443dea51247bc2fe704461a32f26a27986fb63d55" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.4.5" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: d72d7096964baf288b55619fe48100001fc4564ab7923ed0a7f5c7650e03c0d6 + url: "https://pub.dev" + source: hosted + version: "0.3.4" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: "9330309e4400f40e39a2a1d1c340e775d0fd23451cf2dd2286e03c7896fd2bd5" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: "5b36ad2f2b562cffb37212e8d59390b25499bf045b732276e30a207b16a25f61" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + riverpod_lint: + dependency: "direct dev" + description: + name: riverpod_lint + sha256: "70198738c3047ae4f6517ef1a2011a8514a980a52576c7f629a3a08810319a02" + url: "https://pub.dev" + source: hosted + version: "2.1.1" rxdart: dependency: transitive description: @@ -1544,6 +1656,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + vector_map_tiles: + dependency: "direct main" + description: + path: "." + ref: immich_above_4 + resolved-ref: dc685bdbcca2ff2b49b4d0fb77b7bc17fad48608 + url: "https://github.com/shenlong-tanwen/flutter-vector-map-tiles.git" + source: git + version: "4.0.0" vector_math: dependency: transitive description: @@ -1552,6 +1673,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vector_tile: + dependency: transitive + description: + name: vector_tile + sha256: "2ac77f6bbd7ddd97efe059207d67bb7eaf807ab98ad58d99fe41a22c230f44e1" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + vector_tile_renderer: + dependency: transitive + description: + name: vector_tile_renderer + sha256: de212da0f5e48107d3b763a940a428eb1f49d8a4664d41ac0b654f77209a2d0b + url: "https://pub.dev" + source: hosted + version: "4.0.0" video_player: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index e4ea68f78..1e018d2f2 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: 1.84.0+108 +version: 1.89.0+113 isar_version: &isar_version 3.1.0+1 environment: @@ -14,8 +14,9 @@ dependencies: path_provider_ios: photo_manager: ^2.7.2 - flutter_hooks: ^0.18.6 - hooks_riverpod: ^2.2.0 + flutter_hooks: ^0.20.3 + hooks_riverpod: ^2.4.0 + riverpod_annotation: ^2.3.0 cached_network_image: ^3.2.2 flutter_cache_manager: ^3.3.0 intl: ^0.18.0 @@ -28,6 +29,10 @@ dependencies: flutter_map: ^4.0.0 flutter_map_heatmap: ^0.0.4 geolocator: ^10.0.0 # used to move to current location in map view + vector_map_tiles: + git: + url: https://github.com/shenlong-tanwen/flutter-vector-map-tiles.git + ref: immich_above_4 flutter_udid: ^2.0.0 package_info_plus: ^4.1.0 url_launcher: ^6.1.3 @@ -84,6 +89,9 @@ dev_dependencies: mockito: ^5.3.2 integration_test: sdk: flutter + custom_lint: ^0.5.6 + riverpod_lint: ^2.1.0 + riverpod_generator: ^2.3.3 flutter: uses-material-design: true @@ -91,27 +99,28 @@ flutter: - assets/ - assets/i18n/ fonts: - - family: WorkSans - fonts: - - asset: fonts/WorkSans.ttf - - asset: fonts/WorkSans-Italic.ttf - style: italic - # - asset: fonts/WorkSans-Medium.ttf - # weight: 500 - # - asset: fonts/WorkSans-SemiBold.ttf - # weight: 600 - # - asset: fonts/WorkSans-Bold.ttf - # weight: 700 - # - asset: fonts/WorkSans-ExtraBold.ttf - # weight: 800 - # - asset: fonts/WorkSans-Black.ttf - # weight: 900 - family: SnowburstOne fonts: - asset: fonts/SnowburstOne.ttf - family: Inconsolata fonts: - asset: fonts/Inconsolata-Regular.ttf + - family: Overpass + fonts: + - asset: fonts/overpass/Overpass-Regular.ttf + weight: 400 + - asset: fonts/overpass/Overpass-Italic.ttf + style: italic + - asset: fonts/overpass/Overpass-Medium.ttf + weight: 500 + - asset: fonts/overpass/Overpass-SemiBold.ttf + weight: 600 + - asset: fonts/overpass/Overpass-Bold.ttf + weight: 700 + - family: OverpassMono + fonts: + - asset: fonts/overpass/OverpassMono.ttf + flutter_icons: image_path_android: "assets/immich-logo-no-outline.png" image_path_ios: "assets/immich-logo-no-outline.png" diff --git a/mobile/test/builtin_extensions_test.dart b/mobile/test/builtin_extensions_test.dart index 875a20fb0..9fc729774 100644 --- a/mobile/test/builtin_extensions_test.dart +++ b/mobile/test/builtin_extensions_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/utils/builtin_extensions.dart'; +import 'package:immich_mobile/extensions/collection_extensions.dart'; +import 'package:immich_mobile/extensions/string_extensions.dart'; void main() { group('Test toDuration', () { diff --git a/mobile/test/sync_service_test.dart b/mobile/test/sync_service_test.dart index f600ed15a..cc317dc55 100644 --- a/mobile/test/sync_service_test.dart +++ b/mobile/test/sync_service_test.dart @@ -63,8 +63,7 @@ void main() { id: "1", updatedAt: DateTime.now(), email: "a@b.c", - firstName: "first", - lastName: "last", + name: "first last", isAdmin: false, ); setUpAll(() async { diff --git a/nginx/10-listen-on-ipv6-by-default.sh b/nginx/10-listen-on-ipv6-by-default.sh deleted file mode 100755 index e4d85c7cf..000000000 --- a/nginx/10-listen-on-ipv6-by-default.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env sh -# vim:sw=4:ts=4:et - -set -e - -entrypoint_log() { - if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then - echo "$@" - fi -} - -ME=$(basename $0) -DEFAULT_CONF_FILE="etc/nginx/conf.d/default.conf" - -# check if we have ipv6 available -if [ ! -f "/proc/net/if_inet6" ]; then - entrypoint_log "$ME: info: ipv6 not available" - exit 0 -fi - -if [ ! -f "/$DEFAULT_CONF_FILE" ]; then - entrypoint_log "$ME: info: /$DEFAULT_CONF_FILE is not a file or does not exist" - exit 0 -fi - -# check if the file can be modified, e.g. not on a r/o filesystem -touch /$DEFAULT_CONF_FILE 2>/dev/null || { entrypoint_log "$ME: info: can not modify /$DEFAULT_CONF_FILE (read-only file system?)"; exit 0; } - -# check if the file is already modified, e.g. on a container restart -grep -q "listen \[::]\:8080;" /$DEFAULT_CONF_FILE && { entrypoint_log "$ME: info: IPv6 listen already enabled"; exit 0; } - -if [ -f "/etc/os-release" ]; then - . /etc/os-release -else - entrypoint_log "$ME: info: can not guess the operating system" - exit 0 -fi - -# enable ipv6 on default.conf listen sockets -sed -i -E 's,listen 8080;,listen 8080;\n listen [::]:8080;,' /$DEFAULT_CONF_FILE - -entrypoint_log "$ME: info: Enabled listen on IPv6 in /$DEFAULT_CONF_FILE" - -exit 0 diff --git a/nginx/15-set-env-variables.envsh b/nginx/15-set-env-variables.envsh deleted file mode 100755 index 5d4acb1ba..000000000 --- a/nginx/15-set-env-variables.envsh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env sh -set -e - -export IMMICH_WEB_URL="${IMMICH_WEB_URL:-http://immich-web:3000}" -IMMICH_WEB_SCHEME=$(echo "$IMMICH_WEB_URL" | grep -Eo '^https?://' || echo "http://") -export IMMICH_WEB_SCHEME -IMMICH_WEB_HOST=$(echo "$IMMICH_WEB_URL" | cut -d '/' -f 3) -export IMMICH_WEB_HOST -export IMMICH_SERVER_URL="${IMMICH_SERVER_URL:-http://immich-server:3001}" -IMMICH_SERVER_SCHEME=$(echo "$IMMICH_WEB_URL" | grep -Eo '^https?://' || echo "http://") -export IMMICH_SERVER_SCHEME -IMMICH_SERVER_HOST=$(echo "$IMMICH_SERVER_URL" | cut -d '/' -f 3) -export IMMICH_SERVER_HOST diff --git a/nginx/Dockerfile b/nginx/Dockerfile deleted file mode 100644 index 3b8e6f333..000000000 --- a/nginx/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM ghcr.io/nginxinc/nginx-unprivileged:1.25.0-alpine3.17@sha256:e57300e9f60e521c5af3ec8fdc710285a371647e8033bcb8a36020c4394db3e3 - -COPY LICENSE /licenses/LICENSE.txt -COPY LICENSE /LICENSE - -COPY 10-listen-on-ipv6-by-default.sh /docker-entrypoint.d -COPY 15-set-env-variables.envsh /docker-entrypoint.d - -COPY templates/ /etc/nginx/templates diff --git a/nginx/templates/default.conf.template b/nginx/templates/default.conf.template deleted file mode 100644 index 4eca1bd14..000000000 --- a/nginx/templates/default.conf.template +++ /dev/null @@ -1,72 +0,0 @@ -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} - -map $http_x_forwarded_proto $forwarded_protocol { - default $scheme; - - # Only allow the values 'http' and 'https' for the X-Forwarded-Proto header. - http http; - https https; -} - -upstream server { - server ${IMMICH_SERVER_HOST}; - keepalive 2; -} - -upstream web { - server ${IMMICH_WEB_HOST}; - keepalive 2; -} - -server { - listen 8080; - - access_log off; - client_max_body_size 50000M; - - # Compression - gzip on; - gzip_comp_level 2; - gzip_min_length 1000; - gzip_proxied any; - gzip_vary on; - gunzip on; - - # text/html is included by default - gzip_types - application/javascript - application/json - font/ttf - image/svg+xml - text/css; - - proxy_buffering off; - proxy_request_buffering off; - proxy_buffer_size 16k; - proxy_busy_buffers_size 24k; - proxy_buffers 64 4k; - proxy_force_ranges on; - - proxy_http_version 1.1; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $forwarded_protocol; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - - location /api { - - rewrite /api/(.*) /$1 break; - - proxy_pass ${IMMICH_SERVER_SCHEME}server; - } - - location / { - - proxy_pass ${IMMICH_WEB_SCHEME}web; - } -} diff --git a/renovate.json b/renovate.json index a379beb13..c81104d6b 100644 --- a/renovate.json +++ b/renovate.json @@ -1,19 +1,72 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base"], + "extends": ["config:base", "docker:pinDigests"], + "minimumReleaseAge": "5 days", "packageRules": [ { - "matchPaths": ["mobile"], - "groupName": "mobile" + "matchFileNames": ["cli/**"], + "groupName": "@immich/cli", + "matchUpdateTypes": ["minor", "patch"], + "schedule": "on tuesday" }, { - "matchPaths": ["server"], - "groupName": "server" + "matchFileNames": ["docs/**"], + "groupName": "docs", + "matchUpdateTypes": ["minor", "patch"], + "schedule": "on tuesday" }, { - "matchPaths": ["web"], - "groupName": "web" + "matchFileNames": ["mobile/**"], + "groupName": "mobile", + "matchUpdateTypes": ["minor", "patch"], + "schedule": "on tuesday" + }, + { + "matchFileNames": ["server/**"], + "groupName": "server", + "matchUpdateTypes": ["minor", "patch"], + "excludePackagePrefixes": ["exiftool"], + "schedule": "on tuesday" + }, + { + "groupName": "exiftool", + "matchPackagePrefixes": ["exiftool"], + "schedule": "on tuesday" + }, + { + "matchFileNames": ["web/**"], + "groupName": "web", + "matchUpdateTypes": ["minor", "patch"], + "schedule": "on tuesday" + }, + { + "matchFileNames": ["machine-learning/**"], + "groupName": "machine-learning", + "matchUpdateTypes": ["minor", "patch"], + "schedule": "on tuesday" + }, + { + "matchFileNames": [".github/**"], + "groupName": "github-actions", + "schedule": "on tuesday" + }, + { + "groupName": "base-image", + "matchPackagePrefixes": ["ghcr.io/immich-app/base-server"], + "minimumReleaseAge": "0" + }, + { + "matchDatasources": ["docker"], + "matchPackageNames": ["node"], + "versionCompatibility": "^(?[^-]+)(?-.*)?$", + "versioning": "node" } ], - "enabled": false + "ignoreDeps": [ + "http", + "latlong2", + "vector_map_tiles", + "flutter_map", + "flutter_map_heatmap" + ] } diff --git a/server/.dockerignore b/server/.dockerignore deleted file mode 100644 index f4a26f12b..000000000 --- a/server/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules/ -upload/ -dist/ -coverage/ -.reverse-geocoding-dump diff --git a/server/.eslintrc.js b/server/.eslintrc.js index 17a0a2dd6..2e46281fe 100644 --- a/server/.eslintrc.js +++ b/server/.eslintrc.js @@ -19,6 +19,7 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-floating-promises': 'error', + curly: 2, 'prettier/prettier': 0, }, }; diff --git a/server/Dockerfile b/server/Dockerfile index fe843968f..b43058d5a 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,64 +1,43 @@ -FROM node:20.8-bookworm as builder +# dev build +FROM ghcr.io/immich-app/base-server-dev:20231201@sha256:4701c0c5920c78e73040dd2b74d22042ffce393f1a9d3453d90a0ecf81ff8649 as dev WORKDIR /usr/src/app - -COPY bin/install-ffmpeg.sh build-lock.json ./ -RUN sed -i -e's/ main/ main contrib non-free non-free-firmware/g' /etc/apt/sources.list.d/debian.sources -RUN apt-get update && apt-get install -yqq build-essential ninja-build meson pkg-config jq zlib1g autoconf \ -libglib2.0-dev libexpat1-dev librsvg2-dev libexif-dev libwebp-dev liborc-0.4-dev \ -libjpeg62-turbo-dev libgsf-1-dev libspng-dev libjxl-dev libheif-dev liblcms2-2 \ -mesa-va-drivers libmimalloc2.0 $(if [ $(arch) = "x86_64" ]; then echo "intel-media-va-driver-non-free"; fi) \ -&& ./install-ffmpeg.sh && apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* - -# debian build for imagemagick has broken RAW support, so build manually -ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH -ENV LD_RUN_PATH=/usr/local/lib:$LD_RUN_PATH -COPY bin/build-libraw.sh bin/build-imagemagick.sh bin/build-libvips.sh bin/use-camera-wb.patch ./ -RUN ./build-libraw.sh -RUN ./build-imagemagick.sh -RUN ./build-libvips.sh - -COPY package.json package-lock.json ./ - +COPY server/package.json server/package-lock.json ./ RUN npm ci +COPY server . -COPY . . -FROM builder as prod +FROM dev AS prod RUN npm run build RUN npm prune --omit=dev --omit=optional -FROM node:20.8-bookworm - -ENV NODE_ENV=production +# web build +FROM node:iron-alpine3.18 as web WORKDIR /usr/src/app +COPY web/package.json web/package-lock.json ./ +RUN npm ci +COPY web . +RUN npm run build -COPY bin/install-ffmpeg.sh build-lock.json ./ -RUN sed -i -e's/ main/ main contrib non-free non-free-firmware/g' /etc/apt/sources.list.d/debian.sources -RUN apt-get update && apt-get install -yqq tini libheif1 libwebp7 libwebpdemux2 libwebpmux3 mesa-va-drivers \ -libjpeg62-turbo libexpat1 librsvg2-2 libjxl0.7 libspng0 libexif12 libgcc-s1 libglib2.0-0 \ -libgsf-1-114 libopenjp2-7 liblcms2-2 liborc-0.4-0 libopenexr-3-1-30 liblqr-1-0 libltdl7 zlib1g libgomp1 \ -mesa-va-drivers libmimalloc2.0 $(if [ $(arch) = "x86_64" ]; then echo "intel-media-va-driver-non-free"; fi) jq wget \ -&& ./install-ffmpeg.sh && apt-get remove -yqq jq wget && apt-get autoremove -yqq && apt-get clean && rm -rf /var/lib/apt/lists/* \ -&& rm install-ffmpeg.sh && rm build-lock.json -COPY --from=prod /usr/local/lib/ /usr/local/lib/ -RUN ldconfig /usr/local/lib +# prod build +FROM ghcr.io/immich-app/base-server-prod:20231201@sha256:b8e86cf4c3cad872f54bab25a83f7503480049eea5c0ae36a8b8460b13cad3b5 +WORKDIR /usr/src/app +ENV NODE_ENV=production COPY --from=prod /usr/src/app/node_modules ./node_modules COPY --from=prod /usr/src/app/dist ./dist COPY --from=prod /usr/src/app/bin ./bin - +COPY --from=web /usr/src/app/build ./www +COPY server/resources resources +COPY server/package.json server/package-lock.json ./ +COPY server/start*.sh ./ +RUN npm link && npm cache clean --force COPY LICENSE /licenses/LICENSE.txt COPY LICENSE /LICENSE -COPY package.json package-lock.json ./ -COPY start*.sh ./ - -RUN npm link && npm cache clean --force +ENV PATH="${PATH}:/usr/src/app/bin" VOLUME /usr/src/app/upload - EXPOSE 3001 - ENTRYPOINT ["tini", "--", "/bin/sh"] diff --git a/server/LICENSE b/server/LICENSE deleted file mode 100644 index a72f39880..000000000 --- a/server/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Hau Tran - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/server/bin/admin-cli.sh b/server/bin/admin-cli.sh deleted file mode 100755 index b63e331eb..000000000 --- a/server/bin/admin-cli.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -./start.sh admin-cli $1 diff --git a/server/bin/build-imagemagick.sh b/server/bin/build-imagemagick.sh deleted file mode 100755 index e4a57a211..000000000 --- a/server/bin/build-imagemagick.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -set -e - -LOCK=$(jq -c '.packages[] | select(.name == "imagemagick")' build-lock.json) -IMAGEMAGICK_VERSION=${IMAGEMAGICK_VERSION:=$(echo $LOCK | jq -r '.version')} -IMAGEMAGICK_SHA256=${IMAGEMAGICK_SHA256:=$(echo $LOCK | jq -r '.sha256')} - -echo "$IMAGEMAGICK_SHA256 $IMAGEMAGICK_VERSION.tar.gz" > imagemagick.sha256 -mkdir -p ImageMagick -wget -nv https://github.com/ImageMagick/ImageMagick/archive/${IMAGEMAGICK_VERSION}.tar.gz -sha256sum -c imagemagick.sha256 -tar -xvf ${IMAGEMAGICK_VERSION}.tar.gz -C ImageMagick --strip-components=1 -rm ${IMAGEMAGICK_VERSION}.tar.gz -rm imagemagick.sha256 -patch -u ImageMagick/coders/dng.c -i use-camera-wb.patch -cd ImageMagick -./configure --with-modules -make -j$(nproc) -make install -cd .. && rm -rf ImageMagick -ldconfig /usr/local/lib diff --git a/server/bin/build-libraw.sh b/server/bin/build-libraw.sh deleted file mode 100755 index 50bc049ea..000000000 --- a/server/bin/build-libraw.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -set -e - -LOCK=$(jq -c '.packages[] | select(.name == "libraw")' build-lock.json) -LIBRAW_VERSION=${LIBRAW_VERSION:=$(echo $LOCK | jq -r '.version')} -LIBRAW_SHA256=${LIBRAW_SHA256:=$(echo $LOCK | jq -r '.sha256')} - -echo "$LIBRAW_SHA256 $LIBRAW_VERSION.tar.gz" > libraw.sha256 -mkdir -p libraw -wget -nv https://github.com/libraw/libraw/archive/${LIBRAW_VERSION}.tar.gz -sha256sum -c libraw.sha256 -tar -xvf ${LIBRAW_VERSION}.tar.gz -C libraw --strip-components=1 -rm ${LIBRAW_VERSION}.tar.gz -rm libraw.sha256 -cd libraw -autoreconf --install -./configure -make -j$(nproc) -make install -cd .. && rm -rf libraw -ldconfig /usr/local/lib diff --git a/server/bin/build-libvips.sh b/server/bin/build-libvips.sh deleted file mode 100755 index 2c5bc55ba..000000000 --- a/server/bin/build-libvips.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - -set -e - -LOCK=$(jq -c '.packages[] | select(.name == "libvips")' build-lock.json) -LIBVIPS_VERSION=${LIBVIPS_VERSION:=$(echo $LOCK | jq -r '.version')} -LIBVIPS_SHA256=${LIBVIPS_SHA256:=$(echo $LOCK | jq -r '.sha256')} - -echo "$LIBVIPS_SHA256 vips-$LIBVIPS_VERSION.tar.xz" > libvips.sha256 -mkdir -p libvips -wget -nv https://github.com/libvips/libvips/releases/download/v${LIBVIPS_VERSION}/vips-${LIBVIPS_VERSION}.tar.xz -sha256sum -c libvips.sha256 -tar -xvf vips-${LIBVIPS_VERSION}.tar.xz -C libvips --strip-components=1 -rm vips-${LIBVIPS_VERSION}.tar.xz -rm libvips.sha256 -cd libvips -meson setup build --buildtype=release --libdir=lib -Dintrospection=false -Dtiff=disabled -cd build -# ninja test # tests set concurrency too high for arm/v7 -ninja install -cd .. && rm -rf libvips -ldconfig /usr/local/lib diff --git a/server/bin/cli.sh b/server/bin/cli.sh deleted file mode 100755 index 148954d9a..000000000 --- a/server/bin/cli.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -node ./node_modules/immich/bin/index "$@" diff --git a/server/bin/immich b/server/bin/immich new file mode 100755 index 000000000..053e87313 --- /dev/null +++ b/server/bin/immich @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +node /usr/src/app/node_modules/.bin/immich "$@" diff --git a/server/bin/immich-admin b/server/bin/immich-admin new file mode 100755 index 000000000..0634eae4b --- /dev/null +++ b/server/bin/immich-admin @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +/usr/src/app/start.sh immich-admin $1 diff --git a/server/bin/install-ffmpeg.sh b/server/bin/install-ffmpeg.sh deleted file mode 100755 index 46b9e51ef..000000000 --- a/server/bin/install-ffmpeg.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -set -e - -LOCK=$(jq -c '.packages[] | select(.name == "ffmpeg")' build-lock.json) -export TARGETARCH=${TARGETARCH:=$(dpkg --print-architecture)} -FFMPEG_VERSION=${FFMPEG_VERSION:=$(echo $LOCK | jq -r '.version')} -FFMPEG_SHA256=${FFMPEG_SHA256:=$(echo $LOCK | jq -r '.sha256[$ENV.TARGETARCH]')} - -echo "$FFMPEG_SHA256 jellyfin-ffmpeg6_${FFMPEG_VERSION}-bookworm_${TARGETARCH}.deb" > ffmpeg.sha256 - -wget -nv https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_VERSION}/jellyfin-ffmpeg6_${FFMPEG_VERSION}-bookworm_${TARGETARCH}.deb -sha256sum -c ffmpeg.sha256 -apt-get -yqq -f install ./jellyfin-ffmpeg6_${FFMPEG_VERSION}-bookworm_${TARGETARCH}.deb -rm jellyfin-ffmpeg6_${FFMPEG_VERSION}-bookworm_${TARGETARCH}.deb -rm ffmpeg.sha256 -ldconfig /usr/lib/jellyfin-ffmpeg/lib - -ln -s /usr/lib/jellyfin-ffmpeg/ffmpeg /usr/bin -ln -s /usr/lib/jellyfin-ffmpeg/ffprobe /usr/bin diff --git a/server/bin/use-camera-wb.patch b/server/bin/use-camera-wb.patch deleted file mode 100755 index 507afeb3e..000000000 --- a/server/bin/use-camera-wb.patch +++ /dev/null @@ -1,9 +0,0 @@ -@@ -339,6 +339,8 @@ - option=GetImageOption(image_info,"dng:use_camera_wb"); - if (option != (const char *) NULL) - raw_info->params.use_camera_wb=IsStringTrue(option); -+ else -+ raw_info->params.use_camera_wb=MagickTrue; - option=GetImageOption(image_info,"dng:use-auto-wb"); - if (option == (const char *) NULL) - option=GetImageOption(image_info,"dng:use_auto_wb"); diff --git a/server/build-lock.json b/server/build-lock.json deleted file mode 100644 index 691fe5825..000000000 --- a/server/build-lock.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "packages": [ - { - "name": "imagemagick", - "version": "7.1.1-13", - "sha256": "8e3ce1aaad19da9f2ca444072bcc631d193a219e3ee11c13ad6d3c895044142c" - }, - { - "name": "libraw", - "version": "0.21.1", - "sha256": "b63d7ffa43463f74afcc02f9083048c231349b41cc9255dec0840cf8a67b52e0" - }, - { - "name": "libvips", - "version": "8.14.5", - "sha256": "90374e9f6fbd5657b5faf306cacda20658d6144d385316b59b865bc1a487b68d" - }, - { - "name": "ffmpeg", - "version": "6.0-4", - "sha256": { - "amd64": "18d98b292b891cde86c2a08e5e989c3430e51a136cdc232bc4162fef3b4f0f44", - "arm64": "67eb1e5a38ac695dd253d9ac290ad0e9fb709e8260449a7445e8460b7db3c516", - "armhf": "a29605ab0eced3511c8a6623504fab5b8bb174a486d87f94bf5522ed9a5970e6" - } - } - ] -} diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 05aec878a..356399813 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -31,6 +31,14 @@ "$ref": "#/components/schemas/ReactionType" } }, + { + "name": "level", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/ReactionLevel" + } + }, { "name": "userId", "required": false, @@ -1211,6 +1219,51 @@ ] } }, + "/asset/device/{deviceId}": { + "get": { + "description": "Get all asset of a device that are in the database, ID only.", + "operationId": "getAllUserAssetsByDeviceId", + "parameters": [ + { + "name": "deviceId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Asset" + ] + } + }, "/asset/download/archive": { "post": { "operationId": "downloadArchive", @@ -1477,48 +1530,6 @@ ] } }, - "/asset/import": { - "post": { - "operationId": "importFile", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ImportAssetDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetFileUploadResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - } - }, "/asset/jobs": { "post": { "operationId": "runAssetJobs", @@ -1755,51 +1766,6 @@ ] } }, - "/asset/search": { - "post": { - "operationId": "searchAsset", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SearchAssetDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Asset" - ] - } - }, "/asset/search-terms": { "get": { "operationId": "getAssetSearchTerms", @@ -2063,6 +2029,14 @@ "type": "boolean" } }, + { + "name": "withPartners", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, { "name": "timeBucket", "required": true, @@ -2191,6 +2165,14 @@ "type": "boolean" } }, + { + "name": "withPartners", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, { "name": "key", "required": false, @@ -2344,7 +2326,7 @@ }, "/asset/{deviceId}": { "get": { - "description": "Get all asset of a device that are in the database, ID only.", + "deprecated": true, "operationId": "getUserAssetsByDeviceId", "parameters": [ { @@ -2352,7 +2334,6 @@ "required": true, "in": "path", "schema": { - "format": "uuid", "type": "string" } } @@ -2383,6 +2364,7 @@ "api_key": [] } ], + "summary": "Use /asset/device/:deviceId instead - Remove in 1.92 release", "tags": [ "Asset" ] @@ -2440,6 +2422,372 @@ ] } }, + "/assets": { + "get": { + "operationId": "searchAssets", + "parameters": [ + { + "name": "id", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "libraryId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "type", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetTypeEnum" + } + }, + { + "name": "order", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetOrder" + } + }, + { + "name": "deviceAssetId", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "deviceId", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "checksum", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "isArchived", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isEncoded", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isExternal", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isFavorite", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isMotion", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isOffline", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isReadOnly", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "isVisible", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withDeleted", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withStacked", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withExif", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "withPeople", + "required": false, + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "createdBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "createdAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "updatedBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "updatedAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "trashedBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "trashedAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "takenBefore", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "takenAfter", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "originalFileName", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "originalPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "resizePath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "webpPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "encodedVideoPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "city", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "state", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "country", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "make", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "model", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "lensModel", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + }, + { + "name": "size", + "required": false, + "in": "query", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AssetResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Asset" + ] + } + }, "/audit/deletes": { "get": { "operationId": "getAuditDeletes", @@ -2630,14 +2978,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminSignupResponseDto" + "$ref": "#/components/schemas/UserResponseDto" } } }, "description": "" - }, - "400": { - "description": "The server already has an admin" } }, "tags": [ @@ -3486,7 +3831,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/UserResponseDto" + "$ref": "#/components/schemas/PartnerResponseDto" }, "type": "array" } @@ -3563,7 +3908,57 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserResponseDto" + "$ref": "#/components/schemas/PartnerResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Partner" + ] + }, + "put": { + "operationId": "updatePartner", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatePartnerDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PartnerResponseDto" } } }, @@ -4884,6 +5279,47 @@ ] } }, + "/system-config/map/style.json": { + "get": { + "operationId": "getMapStyle", + "parameters": [ + { + "name": "theme", + "required": true, + "in": "query", + "schema": { + "$ref": "#/components/schemas/MapTheme" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "System Config" + ] + } + }, "/system-config/storage-template-options": { "get": { "operationId": "getStorageTemplateOptions", @@ -5466,6 +5902,29 @@ } }, "/user/profile-image": { + "delete": { + "operationId": "deleteProfileImage", + "parameters": [], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "User" + ] + }, "post": { "operationId": "createProfileImage", "parameters": [], @@ -5638,7 +6097,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.84.0", + "version": "1.89.0", "contact": {} }, "tags": [], @@ -5812,34 +6271,6 @@ ], "type": "object" }, - "AdminSignupResponseDto": { - "properties": { - "createdAt": { - "format": "date-time", - "type": "string" - }, - "email": { - "type": "string" - }, - "firstName": { - "type": "string" - }, - "id": { - "type": "string" - }, - "lastName": { - "type": "string" - } - }, - "required": [ - "id", - "email", - "firstName", - "lastName", - "createdAt" - ], - "type": "object" - }, "AlbumCountResponseDto": { "properties": { "notShared": { @@ -5894,6 +6325,9 @@ "id": { "type": "string" }, + "isActivityEnabled": { + "type": "boolean" + }, "lastModifiedAssetTimestamp": { "format": "date-time", "type": "string" @@ -5935,7 +6369,8 @@ "sharedUsers", "hasSharedLink", "assets", - "owner" + "owner", + "isActivityEnabled" ], "type": "object" }, @@ -6014,6 +6449,9 @@ }, "AssetBulkUpdateDto": { "properties": { + "dateTimeOriginal": { + "type": "string" + }, "ids": { "items": { "format": "uuid", @@ -6027,6 +6465,12 @@ "isFavorite": { "type": "boolean" }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, "removeParent": { "type": "boolean" }, @@ -6193,6 +6637,13 @@ ], "type": "object" }, + "AssetOrder": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, "AssetResponseDto": { "properties": { "checksum": { @@ -6547,15 +6998,6 @@ ], "type": "object" }, - "CitiesFile": { - "enum": [ - "cities15000", - "cities5000", - "cities1000", - "cities500" - ], - "type": "string" - }, "ClassificationConfig": { "properties": { "enabled": { @@ -6756,15 +7198,12 @@ "nullable": true, "type": "string" }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, "memoriesEnabled": { "type": "boolean" }, + "name": { + "type": "string" + }, "password": { "type": "string" }, @@ -6776,8 +7215,7 @@ "required": [ "email", "password", - "firstName", - "lastName" + "name" ], "type": "object" }, @@ -7105,64 +7543,6 @@ ], "type": "object" }, - "ImportAssetDto": { - "properties": { - "assetPath": { - "type": "string" - }, - "deviceAssetId": { - "type": "string" - }, - "deviceId": { - "type": "string" - }, - "duration": { - "type": "string" - }, - "fileCreatedAt": { - "format": "date-time", - "type": "string" - }, - "fileModifiedAt": { - "format": "date-time", - "type": "string" - }, - "isArchived": { - "type": "boolean" - }, - "isExternal": { - "type": "boolean" - }, - "isFavorite": { - "type": "boolean" - }, - "isOffline": { - "type": "boolean" - }, - "isReadOnly": { - "default": true, - "type": "boolean" - }, - "isVisible": { - "type": "boolean" - }, - "libraryId": { - "format": "uuid", - "type": "string" - }, - "sidecarPath": { - "type": "string" - } - }, - "required": [ - "assetPath", - "deviceAssetId", - "deviceId", - "fileCreatedAt", - "fileModifiedAt" - ], - "type": "object" - }, "JobCommand": { "enum": [ "start", @@ -7373,35 +7753,24 @@ "LoginResponseDto": { "properties": { "accessToken": { - "readOnly": true, - "type": "string" - }, - "firstName": { - "readOnly": true, "type": "string" }, "isAdmin": { - "readOnly": true, "type": "boolean" }, - "lastName": { - "readOnly": true, + "name": { "type": "string" }, "profileImagePath": { - "readOnly": true, "type": "string" }, "shouldChangePassword": { - "readOnly": true, "type": "boolean" }, "userEmail": { - "readOnly": true, "type": "string" }, "userId": { - "readOnly": true, "type": "string" } }, @@ -7409,8 +7778,7 @@ "accessToken", "userId", "userEmail", - "firstName", - "lastName", + "name", "profileImagePath", "isAdmin", "shouldChangePassword" @@ -7453,6 +7821,13 @@ ], "type": "object" }, + "MapTheme": { + "enum": [ + "light", + "dark" + ], + "type": "string" + }, "MemoryLaneResponseDto": { "properties": { "assets": { @@ -7551,6 +7926,77 @@ ], "type": "object" }, + "PartnerResponseDto": { + "properties": { + "avatarColor": { + "$ref": "#/components/schemas/UserAvatarColor" + }, + "createdAt": { + "format": "date-time", + "type": "string" + }, + "deletedAt": { + "format": "date-time", + "nullable": true, + "type": "string" + }, + "email": { + "type": "string" + }, + "externalPath": { + "nullable": true, + "type": "string" + }, + "id": { + "type": "string" + }, + "inTimeline": { + "type": "boolean" + }, + "isAdmin": { + "type": "boolean" + }, + "memoriesEnabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "oauthId": { + "type": "string" + }, + "profileImagePath": { + "type": "string" + }, + "shouldChangePassword": { + "type": "boolean" + }, + "storageLabel": { + "nullable": true, + "type": "string" + }, + "updatedAt": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "avatarColor", + "id", + "name", + "email", + "profileImagePath", + "storageLabel", + "externalPath", + "shouldChangePassword", + "isAdmin", + "createdAt", + "deletedAt", + "updatedAt", + "oauthId" + ], + "type": "object" + }, "PathEntityType": { "enum": [ "asset", @@ -7715,6 +8161,13 @@ ], "type": "object" }, + "ReactionLevel": { + "enum": [ + "album", + "asset" + ], + "type": "string" + }, "ReactionType": { "enum": [ "comment", @@ -7793,17 +8246,6 @@ ], "type": "object" }, - "SearchAssetDto": { - "properties": { - "searchTerm": { - "type": "string" - } - }, - "required": [ - "searchTerm" - ], - "type": "object" - }, "SearchAssetResponseDto": { "properties": { "count": { @@ -7922,9 +8364,6 @@ "loginPageMessage": { "type": "string" }, - "mapTileUrl": { - "type": "string" - }, "oauthButtonText": { "type": "string" }, @@ -7936,7 +8375,6 @@ "trashDays", "oauthButtonText", "loginPageMessage", - "mapTileUrl", "isInitialized" ], "type": "object" @@ -8303,14 +8741,10 @@ "example": "testuser@email.com", "type": "string" }, - "firstName": { + "name": { "example": "Admin", "type": "string" }, - "lastName": { - "example": "Doe", - "type": "string" - }, "password": { "example": "password", "type": "string" @@ -8319,8 +8753,7 @@ "required": [ "email", "password", - "firstName", - "lastName" + "name" ], "type": "object" }, @@ -8587,16 +9020,20 @@ }, "SystemConfigMapDto": { "properties": { + "darkStyle": { + "type": "string" + }, "enabled": { "type": "boolean" }, - "tileUrl": { + "lightStyle": { "type": "string" } }, "required": [ "enabled", - "tileUrl" + "lightStyle", + "darkStyle" ], "type": "object" }, @@ -8675,15 +9112,11 @@ }, "SystemConfigReverseGeocodingDto": { "properties": { - "citiesFileOverride": { - "$ref": "#/components/schemas/CitiesFile" - }, "enabled": { "type": "boolean" } }, "required": [ - "citiesFileOverride", "enabled" ], "type": "object" @@ -8910,12 +9343,18 @@ }, "description": { "type": "string" + }, + "isActivityEnabled": { + "type": "boolean" } }, "type": "object" }, "UpdateAssetDto": { "properties": { + "dateTimeOriginal": { + "type": "string" + }, "description": { "type": "string" }, @@ -8924,6 +9363,12 @@ }, "isFavorite": { "type": "boolean" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" } }, "type": "object" @@ -8951,6 +9396,17 @@ }, "type": "object" }, + "UpdatePartnerDto": { + "properties": { + "inTimeline": { + "type": "boolean" + } + }, + "required": [ + "inTimeline" + ], + "type": "object" + }, "UpdateStackParentDto": { "properties": { "newParentId": { @@ -8978,15 +9434,15 @@ }, "UpdateUserDto": { "properties": { + "avatarColor": { + "$ref": "#/components/schemas/UserAvatarColor" + }, "email": { "type": "string" }, "externalPath": { "type": "string" }, - "firstName": { - "type": "string" - }, "id": { "format": "uuid", "type": "string" @@ -8994,12 +9450,12 @@ "isAdmin": { "type": "boolean" }, - "lastName": { - "type": "string" - }, "memoriesEnabled": { "type": "boolean" }, + "name": { + "type": "string" + }, "password": { "type": "string" }, @@ -9024,13 +9480,10 @@ "format": "int64", "type": "integer" }, - "userFirstName": { - "type": "string" - }, "userId": { "type": "string" }, - "userLastName": { + "userName": { "type": "string" }, "videos": { @@ -9039,26 +9492,40 @@ }, "required": [ "userId", - "userFirstName", - "userLastName", + "userName", "photos", "videos", "usage" ], "type": "object" }, + "UserAvatarColor": { + "enum": [ + "primary", + "pink", + "red", + "yellow", + "blue", + "green", + "purple", + "orange", + "gray", + "amber" + ], + "type": "string" + }, "UserDto": { "properties": { - "email": { - "type": "string" + "avatarColor": { + "$ref": "#/components/schemas/UserAvatarColor" }, - "firstName": { + "email": { "type": "string" }, "id": { "type": "string" }, - "lastName": { + "name": { "type": "string" }, "profileImagePath": { @@ -9066,9 +9533,9 @@ } }, "required": [ + "avatarColor", "id", - "firstName", - "lastName", + "name", "email", "profileImagePath" ], @@ -9076,6 +9543,9 @@ }, "UserResponseDto": { "properties": { + "avatarColor": { + "$ref": "#/components/schemas/UserAvatarColor" + }, "createdAt": { "format": "date-time", "type": "string" @@ -9092,21 +9562,18 @@ "nullable": true, "type": "string" }, - "firstName": { - "type": "string" - }, "id": { "type": "string" }, "isAdmin": { "type": "boolean" }, - "lastName": { - "type": "string" - }, "memoriesEnabled": { "type": "boolean" }, + "name": { + "type": "string" + }, "oauthId": { "type": "string" }, @@ -9126,9 +9593,9 @@ } }, "required": [ + "avatarColor", "id", - "firstName", - "lastName", + "name", "email", "profileImagePath", "storageLabel", diff --git a/server/package-lock.json b/server/package-lock.json index 6eb1fb7f3..60367eb33 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,15 +1,16 @@ { "name": "immich", - "version": "1.84.0", + "version": "1.89.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.84.0", + "version": "1.89.0", "license": "UNLICENSED", "dependencies": { "@babel/runtime": "^7.22.11", + "@immich/cli": "^2.0.3", "@nestjs/bullmq": "^10.0.1", "@nestjs/common": "^10.2.2", "@nestjs/config": "^3.0.0", @@ -28,17 +29,15 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", - "exiftool-vendored": "~23.1.0", - "exiftool-vendored.pl": "12.67", + "exiftool-vendored": "~23.5.0", + "exiftool-vendored.pl": "12.70", "fluent-ffmpeg": "^2.1.2", "geo-tz": "^7.0.7", "glob": "^10.3.3", "handlebars": "^4.7.8", "i18n-iso-countries": "^7.6.0", - "immich": "^0.41.0", "ioredis": "^5.3.2", "joi": "^17.10.0", - "local-reverse-geocoder": "0.16.5", "lodash": "^4.17.21", "luxon": "^3.4.2", "mv": "^2.1.1", @@ -54,24 +53,20 @@ "typesense": "^1.7.1", "ua-parser-js": "^1.0.35" }, - "bin": { - "immich": "bin/cli.sh", - "immich-admin": "bin/admin-cli.sh" - }, "devDependencies": { "@nestjs/cli": "^10.1.16", "@nestjs/schematics": "^10.0.2", "@nestjs/testing": "^10.2.2", "@openapitools/openapi-generator-cli": "2.7.0", "@testcontainers/postgresql": "^10.2.1", - "@types/archiver": "^5.3.2", + "@types/archiver": "^6.0.0", "@types/bcrypt": "^5.0.0", "@types/cookie-parser": "^1.4.3", "@types/cron": "^2.0.1", "@types/express": "^4.17.17", "@types/fluent-ffmpeg": "^2.1.21", "@types/imagemin": "^8.0.1", - "@types/jest": "29.5.4", + "@types/jest": "29.5.10", "@types/jest-when": "^3.5.2", "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", @@ -94,6 +89,7 @@ "prettier-plugin-organize-imports": "^3.2.3", "rimraf": "^5.0.1", "source-map-support": "^0.5.21", + "sql-formatter": "^14.0.0", "supertest": "^6.3.3", "testcontainers": "^10.2.1", "ts-jest": "^29.1.1", @@ -127,9 +123,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.3.tgz", - "integrity": "sha512-oZLdg2XTx7likYAXRj1CU0XmrsCfe5f2grj3iwuI3OB1LXwwpdbHBztruj03y3yHES+TnO+dIbkvRnvMXs7uAA==", + "version": "16.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.8.tgz", + "integrity": "sha512-PTGozYvh1Bin5lB15PwcXa26Ayd17bWGLS3H8Rs0s+04mUDvfNofmweaX1LgumWWy3nCUTDuwHxX10M3G0wE2g==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -154,12 +150,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.3.tgz", - "integrity": "sha512-+lBiHxi/C9HCfiCbtW25DldwvJDXXXv5oWw+Tg4s18BO/lYZLveGUEaZWu9ZJ5VIJ8GliUi2LohxhDxBkh4Oxg==", + "version": "16.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.8.tgz", + "integrity": "sha512-MBiKZOlR9/YMdflALr7/7w/BGAfo/BGTrlkqsIB6rDWV1dYiCgxI+033HsiNssLS6RQyCFx/e7JA2aBBzu9zEg==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.3", + "@angular-devkit/core": "16.2.8", "jsonc-parser": "3.2.0", "magic-string": "0.30.1", "ora": "5.4.1", @@ -172,13 +168,13 @@ } }, "node_modules/@angular-devkit/schematics-cli": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-16.2.3.tgz", - "integrity": "sha512-5YQCbQmY9Kc03a9Io4XHOrxGXjnzcVveUuUO64R1m5x2aA5I+mVR8NVvxuoGRAeoI1FWusAKRe9hH8nRCLrelA==", + "version": "16.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-16.2.8.tgz", + "integrity": "sha512-EXURJCzWTVYCipiTT4vxQQOrF63asOUDbeOy3OtiSh7EwIUvxm3BPG6hquJqngEnI/N6bA75NJ1fBhU6Hrh7eA==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.3", - "@angular-devkit/schematics": "16.2.3", + "@angular-devkit/core": "16.2.8", + "@angular-devkit/schematics": "16.2.8", "ansi-colors": "4.1.3", "inquirer": "8.2.4", "symbol-observable": "4.0.0", @@ -805,9 +801,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", - "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", + "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -942,9 +938,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -987,9 +983,9 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1021,12 +1017,12 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -1048,11 +1044,36 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@immich/cli": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@immich/cli/-/cli-2.0.4.tgz", + "integrity": "sha512-Fzi4sESi1pD0YdqhDAlGgR8WfeBEsfxuNpbzgY6DklmgTeIDOh29pv+ESo080uWfKhXdBBhYgc1ZUc6m5krxQw==", + "dependencies": { + "axios": "^1.6.2", + "byte-size": "^8.1.1", + "cli-progress": "^3.12.0", + "commander": "^11.0.0", + "form-data": "^4.0.0", + "glob": "^10.3.1", + "yaml": "^2.3.1" + }, + "bin": { + "immich": "dist/src/index.js" + } + }, + "node_modules/@immich/cli/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "engines": { + "node": ">=16" + } + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -1710,20 +1731,21 @@ } }, "node_modules/@nestjs/cli": { - "version": "10.1.18", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.1.18.tgz", - "integrity": "sha512-jQtG47keLsACt7b4YwJbTBYRm90n82gJpMaiR1HGAyQ9pccbctjSYu592eT4bxqkUWxPgBE3mpNynXj7dWAfrw==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.2.1.tgz", + "integrity": "sha512-CAJAQwmxFZfB3RTvqz/eaXXWpyU+mZ4QSqfBYzjneTsPgF+uyOAW3yQpaLNn9Dfcv39R9UxSuAhayv6yuFd+Jg==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.2.3", - "@angular-devkit/schematics": "16.2.3", - "@angular-devkit/schematics-cli": "16.2.3", + "@angular-devkit/core": "16.2.8", + "@angular-devkit/schematics": "16.2.8", + "@angular-devkit/schematics-cli": "16.2.8", "@nestjs/schematics": "^10.0.1", "chalk": "4.1.2", "chokidar": "3.5.3", "cli-table3": "0.6.3", "commander": "4.1.1", - "fork-ts-checker-webpack-plugin": "8.0.0", + "fork-ts-checker-webpack-plugin": "9.0.2", + "glob": "10.3.10", "inquirer": "8.2.6", "node-emoji": "1.11.0", "ora": "5.4.1", @@ -1735,14 +1757,14 @@ "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.2.2", - "webpack": "5.88.2", + "webpack": "5.89.0", "webpack-node-externals": "3.0.0" }, "bin": { "nest": "bin/nest.js" }, "engines": { - "node": ">= 16" + "node": ">= 16.14" }, "peerDependencies": { "@swc/cli": "^0.1.62", @@ -1766,24 +1788,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/@nestjs/cli/node_modules/glob": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", - "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "minimatch": "^8.0.2", - "minipass": "^4.2.4", - "path-scurry": "^1.6.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@nestjs/cli/node_modules/minimatch": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", @@ -1826,10 +1830,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@nestjs/cli/node_modules/rimraf/node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nestjs/cli/node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@nestjs/common": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.6.tgz", - "integrity": "sha512-ma8R7n+FXsWM4XF9QXjjrsRceyRzid/xKmNKVOa/sTJntkVG8lL71BHBEfjtFvO6EJUqjs/15LbDc0iaN5nCwA==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.10.tgz", + "integrity": "sha512-fwAk931rjW8CNH2Mgwawq/7HWHH1dxkOLdcgs7U52ddLk8CtHXjejm1cbNahewlSbNhvlOl7y1STLHutE6sUqw==", "dependencies": { "iterare": "1.2.1", "tslib": "2.6.2", @@ -1875,9 +1910,9 @@ } }, "node_modules/@nestjs/core": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.6.tgz", - "integrity": "sha512-oGQ2CoBeFRT7egG47MFqS89xlXBTIRZBkRpKRTPMftEfL1RMXhXIcIIaGfzp11wx6qxrBVxBXpVLM09oaqHpaQ==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.10.tgz", + "integrity": "sha512-+ckOI6BPi2ZMHikT9MCG4ctHDc4OnjhoIytrn7f2AYMMXI4bnutJhqyQKc30VDka5x3Wq6QAD57pgSP7y+JjJg==", "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", @@ -1917,9 +1952,9 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@nestjs/mapped-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.2.tgz", - "integrity": "sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.3.tgz", + "integrity": "sha512-40Zdqg98lqoF0+7ThWIZFStxgzisK6GG22+1ABO4kZiGF/Tu2FE+DYLw+Q9D94vcFWizJ+MSjNN4ns9r6hIGxw==", "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "class-transformer": "^0.4.0 || ^0.5.0", @@ -1936,9 +1971,9 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.6.tgz", - "integrity": "sha512-4U16D5ot2570CR8Qm5qu/SBXsA2l5KxN7AVSGvzoWoBxjEoOnnZOapC5Pler3yYa0tT1xLhji61RX1gceKW3dw==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.10.tgz", + "integrity": "sha512-U4KDgtMjH8TqEvt0RzC/POP8ABvL9bYoCScvlGtFSKgVGaMLBKkZ4+jHtbQx6qItYSlBBRUuz/dveMZCObfrkQ==", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", @@ -1961,9 +1996,9 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@nestjs/platform-socket.io": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.2.6.tgz", - "integrity": "sha512-c4GbHeyd12hyrLngnHDouBui0fwPjaK4TopkZdvkskRCd4sOfph9EUX2CHny+ZL8UeY8IbW/yBZxQ746ahSYsQ==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.2.10.tgz", + "integrity": "sha512-JBuemeIBp2mpp+z7D12oa22k83TnDTxyQDMKZpO/B2/QnBVR+2C4EZ07/XOct14LQXO6vIjmT0iYYCZbNvczjw==", "dependencies": { "socket.io": "4.7.2", "tslib": "2.6.2" @@ -2010,13 +2045,13 @@ } }, "node_modules/@nestjs/schematics": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.0.2.tgz", - "integrity": "sha512-DaZZjymYoIfRqC5W62lnYXIIods1PDY6CGc8+IpRwyinzffjKxZ3DF3exu+mdyvllzkXo9DTXkoX4zOPSJHCkw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.0.3.tgz", + "integrity": "sha512-2BRujK0GqGQ7j1Zpz+obVfskDnnOeVKt5aXoSaVngKo8Oczy8uYCY+R547TQB+Kf35epdfFER2pVnQrX3/It5A==", "dev": true, "dependencies": { - "@angular-devkit/core": "16.1.8", - "@angular-devkit/schematics": "16.1.8", + "@angular-devkit/core": "16.2.8", + "@angular-devkit/schematics": "16.2.8", "comment-json": "4.2.3", "jsonc-parser": "3.2.0", "pluralize": "8.0.0" @@ -2025,72 +2060,16 @@ "typescript": ">=4.8.2" } }, - "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.8.tgz", - "integrity": "sha512-dSRD/+bGanArIXkj+kaU1kDFleZeQMzmBiOXX+pK0Ah9/0Yn1VmY3RZh1zcX9vgIQXV+t7UPrTpOjaERMUtVGw==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.8.tgz", - "integrity": "sha512-6LyzMdFJs337RTxxkI2U1Ndw0CW5mMX/aXWl8d7cW2odiSrAg8IdlMqpc+AM8+CPfsB0FtS1aWkEZqJLT0jHOg==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "16.1.8", - "jsonc-parser": "3.2.0", - "magic-string": "0.30.0", - "ora": "5.4.1", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@nestjs/schematics/node_modules/magic-string": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", - "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@nestjs/swagger": { - "version": "7.1.12", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.12.tgz", - "integrity": "sha512-Q1P/IE+cws0sJeNtbs+8uDalcVylpmAnaEUFenGOa3KSNnXF/8DOE84mET/uUhFXsiz9PLHK8Hy7o7B6fRpMhg==", + "version": "7.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.16.tgz", + "integrity": "sha512-f9KBk/BX9MUKPTj7tQNYJ124wV/jP5W2lwWHLGwe/4qQXixuDOo39zP55HIJ44LE7S04B7BOeUOo9GBJD/vRcw==", "dependencies": { - "@nestjs/mapped-types": "2.0.2", + "@nestjs/mapped-types": "2.0.3", "js-yaml": "4.1.0", "lodash": "4.17.21", "path-to-regexp": "3.2.0", - "swagger-ui-dist": "5.7.2" + "swagger-ui-dist": "5.9.1" }, "peerDependencies": { "@fastify/static": "^6.0.0", @@ -2113,9 +2092,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.6.tgz", - "integrity": "sha512-uxlxHhpSvG4yDTPmuPneoQL1/UnBkOkzE+Zaz6bwURg7lc3uS4ZsXl75OL3pYaJH37rHYXYT9bGcYSpxVbwIrg==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.10.tgz", + "integrity": "sha512-IVLUnPz/+fkBtPATYfqTIP+phN9yjkXejmj+JyhmcfPJZpxBmD1i9VSMqa4u54l37j0xkGPscQ0IXpbhqMYUKw==", "dev": true, "dependencies": { "tslib": "2.6.2" @@ -2146,11 +2125,11 @@ "dev": true }, "node_modules/@nestjs/typeorm": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.0.tgz", - "integrity": "sha512-WQU4HCDTz4UavsFzvGUKDHqi0MO5K47yFoPXdmh+Z/hCNO7SHCMmV9jLiLukM8n5nKUqJ3jDqiljkWBcZPdCtA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.1.tgz", + "integrity": "sha512-YVFYL7D25VAVp5/G+KLXIgsRfYomA+VaFZBpm2rtwrrBOmkXNrxr7kuI2bBBO/Xy4kKBDe6wbvIVVFeEA7/ngA==", "dependencies": { - "uuid": "9.0.0" + "uuid": "9.0.1" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", @@ -2160,10 +2139,22 @@ "typeorm": "^0.3.0" } }, + "node_modules/@nestjs/typeorm/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@nestjs/websockets": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.2.6.tgz", - "integrity": "sha512-HwZADfixAMKMdMB/eBz0HJnPCs0r+W+5inpRwCazsQhwZniGUgXkfIhyRvNfHip/nb+DLS/M8BNBR2JGiJNTEg==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.2.10.tgz", + "integrity": "sha512-L1AkxwLUj/ntk26jO1SXYl3GRElQE6Fikzfy/3MPFURk0GDs7tHUzLcb8BC8q8u5ZpUjBAC2wFVQzrY5R0MHNw==", "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -2735,12 +2726,12 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "node_modules/@testcontainers/postgresql": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.2.1.tgz", - "integrity": "sha512-snIB11wyHUYPzQNNgoHpiRcZO3sR0mDwU28cwOd0lAT4kzTvTihZTmz4gv8MYppcXlRsLAIE0QqzGauZsfFQiA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.3.2.tgz", + "integrity": "sha512-PhbmhWe+M5RF9QZGOzg/Vc+Ve6KlT/j9OLv9G11xubvcdhbuAz9Am3u1GjsXS7C1Vt/rwWx+j0kg+FtYwbJQng==", "dev": true, "dependencies": { - "testcontainers": "^10.2.1" + "testcontainers": "^10.3.2" } }, "node_modules/@tsconfig/node10": { @@ -2799,9 +2790,9 @@ } }, "node_modules/@types/archiver": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.3.tgz", - "integrity": "sha512-0ABdVcXL6jOwNGY+hjWPqrxUvKelBEwNLcuv/SV2vZ4YCH8w9NttFCt+/QqI5zgMX+iX/XqVy89/r7EmLJmMpQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.2.tgz", + "integrity": "sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==", "dev": true, "dependencies": { "@types/readdir-glob": "*" @@ -2849,9 +2840,9 @@ } }, "node_modules/@types/bcrypt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", - "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -2882,9 +2873,9 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "node_modules/@types/cookie-parser": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.4.tgz", - "integrity": "sha512-Var+aj5I6ZgIqsQ05N2V8q5OBrFfZXtIGWWDSrEYLIbMw758obagSwdGcLCjwh1Ga7M7+wj0SDIAaAC/WT7aaA==", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==", "dev": true, "dependencies": { "@types/express": "*" @@ -2914,6 +2905,26 @@ "cron": "*" } }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.23", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.23.tgz", + "integrity": "sha512-Lz5J+NFgZS4cEVhquwjIGH4oQwlVn2h7LXD3boitujBnzOE5o7s9H8hchEjoDK2SlRsJTogdKnQeiJgPPKLIEw==", + "dev": true, + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "8.44.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", @@ -2941,9 +2952,9 @@ "dev": true }, "node_modules/@types/express": { - "version": "4.17.18", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", - "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dev": true, "dependencies": { "@types/body-parser": "*", @@ -2965,9 +2976,9 @@ } }, "node_modules/@types/fluent-ffmpeg": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.22.tgz", - "integrity": "sha512-ZZPDDrDOb2Ahp5fxZzuw64f0rCcviv+SDuCyJ1PIF/UFn9wNHtb/bY8Dj/2nrbQ7SNsGI7gaO2wJVkkU2HBcMg==", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.24.tgz", + "integrity": "sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==", "dev": true, "dependencies": { "@types/node": "*" @@ -2989,9 +3000,9 @@ "dev": true }, "node_modules/@types/imagemin": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.1.tgz", - "integrity": "sha512-DSpM//dRPzme7doePGkmR1uoquHi0h0ElaA5qFnxHECfFcB8z/jhMI8eqmxWNpHn9ZG18p4PC918sZLhR0cr5A==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.5.tgz", + "integrity": "sha512-tah3dm+5sG+fEDAz6CrQ5evuEaPX9K6DF3E5a01MPOKhA2oGBoC+oA5EJzSugB905sN4DE19EDzldT2Cld2g6Q==", "dev": true, "dependencies": { "@types/node": "*" @@ -3032,9 +3043,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", - "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", + "version": "29.5.10", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", + "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -3042,9 +3053,9 @@ } }, "node_modules/@types/jest-when": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@types/jest-when/-/jest-when-3.5.3.tgz", - "integrity": "sha512-gJWLSCRWEkPHYXYjjrxEMfWopoA2bl9nUbwDomxIzRXfe0gLkmSDPD0lLcoAiO2RDpXDoHSeigy/rNa/ja0MfQ==", + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@types/jest-when/-/jest-when-3.5.5.tgz", + "integrity": "sha512-H9MDPIrz7NOu6IXP9OHExNN9LnJbGYAzRsGIDKxWr7Fth9vovemNV8yFbkUWLSEmuA8PREvAEvt9yK0PPLmFHA==", "dev": true, "dependencies": { "@types/jest": "*" @@ -3057,15 +3068,15 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.199", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", - "integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==", + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", "dev": true }, "node_modules/@types/luxon": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz", - "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==" + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.5.tgz", + "integrity": "sha512-1cyf6Ge/94zlaWIZA2ei1pE6SZ8xpad2hXaYa5JEFiaUH0YS494CZwyi4MXNpXD9oEuv6ZH0Bmh0e7F9sPhmZA==" }, "node_modules/@types/mime": { "version": "1.3.3", @@ -3074,39 +3085,36 @@ "dev": true }, "node_modules/@types/mock-fs": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.2.tgz", - "integrity": "sha512-mSIMAOjrNTVUFmZgJEigSIm+GlS4hbrk8U5+M8EB45uMrykKdN9TidjjSaOY1yFph2+TD7bsIfB4r+IrMYVyPQ==", + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz", + "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/multer": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.8.tgz", - "integrity": "sha512-VMZOW6mnmMMhA5m3fsCdXBwFwC+a+27/8gctNMuQC4f7UtWcF79KAFGoIfKZ4iqrElgWIa3j5vhMJDp0iikQ1g==", + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", "dev": true, "dependencies": { "@types/express": "*" } }, "node_modules/@types/mv": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/mv/-/mv-2.1.2.tgz", - "integrity": "sha512-IvAjPuiQ2exDicnTrMidt1m+tj3gZ60BM0PaoRsU0m9Cn+lrOyemuO9Tf8CvHFmXlxMjr1TVCfadi9sfwbSuKg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mv/-/mv-2.1.4.tgz", + "integrity": "sha512-MgEHBpXnQo44Q43j8G0Bvp/Yi8LYbC8hxKrRFMgDEDZMmzDKZLgiyMWtW49B37ko+QupgZ3G5rtPUnOGe5ixLw==", "dev": true }, "node_modules/@types/node": { - "version": "20.8.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", - "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==" - }, - "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true + "version": "20.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", + "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/qs": { "version": "6.9.8", @@ -3130,9 +3138,9 @@ } }, "node_modules/@types/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/send": { @@ -3201,9 +3209,9 @@ } }, "node_modules/@types/supertest": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.13.tgz", - "integrity": "sha512-Vc/5/pRwSC055fU7Wu8erTj4gLpID9SdG2zRMuqaHLni3GTsrJ8gyB6MbFZZGLW6vQaGPhiUWRB6uWglv87MEg==", + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.16.tgz", + "integrity": "sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==", "dev": true, "dependencies": { "@types/superagent": "*" @@ -3219,9 +3227,9 @@ } }, "node_modules/@types/ua-parser-js": { - "version": "0.7.37", - "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.37.tgz", - "integrity": "sha512-4sOxS3ZWXC0uHJLYcWAaLMxTvjRX3hT96eF4YWUh1ovTaenvibaZOE5uXtIp4mksKMLRwo7YDiCBCw6vBiUPVg==", + "version": "0.7.39", + "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz", + "integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==", "dev": true }, "node_modules/@types/validator": { @@ -3245,16 +3253,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz", - "integrity": "sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", + "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/type-utils": "6.7.4", - "@typescript-eslint/utils": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/type-utils": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -3280,15 +3288,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.4.tgz", - "integrity": "sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/typescript-estree": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4" }, "engines": { @@ -3308,13 +3316,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz", - "integrity": "sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4" + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3325,13 +3333,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.4.tgz", - "integrity": "sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", + "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.7.4", - "@typescript-eslint/utils": "6.7.4", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/utils": "6.13.1", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -3352,9 +3360,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.4.tgz", - "integrity": "sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3365,13 +3373,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz", - "integrity": "sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3392,17 +3400,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.4.tgz", - "integrity": "sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/typescript-estree": "6.7.4", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", "semver": "^7.5.4" }, "engines": { @@ -3417,12 +3425,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz", - "integrity": "sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/types": "6.13.1", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -3433,6 +3441,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -3933,9 +3947,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -4134,18 +4148,6 @@ "node": ">=0.6" } }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - }, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -4331,14 +4333,6 @@ "node": ">=4" } }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", - "engines": { - "node": ">=0.2.0" - } - }, "node_modules/buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -4350,9 +4344,9 @@ } }, "node_modules/bullmq": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.12.0.tgz", - "integrity": "sha512-eAN7WnlR6MszikwQXE16oGMUWngfbYG0SqkxiwUmVm4muR3KYg+etIRZE3vRKNXWKw0WxJZTA+3oBfDMcCBh+w==", + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.4.tgz", + "integrity": "sha512-8tD3Zq4CP+2q+zZ1JSkKov5ra18Jhth8zdr2X1/V2MMOVpa8vfVCsQaRnKgDpiMpaF6AH9sucXUNtk4xamTEKw==", "dependencies": { "cron-parser": "^4.6.0", "glob": "^8.0.3", @@ -4437,6 +4431,14 @@ "node": ">=0.10.0" } }, + "node_modules/byte-size": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", + "integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==", + "engines": { + "node": ">=12.17" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4494,17 +4496,6 @@ } ] }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", - "dependencies": { - "traverse": ">=0.3.0 <0.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5025,19 +5016,28 @@ } }, "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/cpu-features": { @@ -5146,19 +5146,6 @@ "node": ">= 8" } }, - "node_modules/csv-parse": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.0.tgz", - "integrity": "sha512-RxruSK3M4XgzcD7Trm2wEN+SJ26ChIb903+IWxNOcB5q4jT2Cs+hFr6QP39J05EohshRFEvyzEBoZ/466S2sbw==" - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "engines": { - "node": ">= 12" - } - }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -5514,6 +5501,12 @@ "node": ">=8" } }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", + "dev": true + }, "node_modules/docker-compose": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.2.tgz", @@ -5526,15 +5519,6 @@ "node": ">= 6.0.0" } }, - "node_modules/docker-compose/node_modules/yaml": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/docker-modem": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.8.tgz", @@ -5768,18 +5752,19 @@ } }, "node_modules/eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.54.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -5834,9 +5819,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", - "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", + "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", @@ -6043,34 +6028,34 @@ "dev": true }, "node_modules/exiftool-vendored": { - "version": "23.1.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-23.1.0.tgz", - "integrity": "sha512-sZ1OUpvAWbUCCoidMMKDTTJ3hHE3mHxb4ihWKmta/eQYYMR54Mssp6+Nf7HoFvY//nX5YK2VCOGVexGGuhM8Bw==", + "version": "23.5.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-23.5.0.tgz", + "integrity": "sha512-6wlCNFFOcOcRa8GpuCzg0dhOcRMipONMZidP3jYUF0g7YkhknKxG4aA0BdmeH2Tp3Hm0286egicslGW9d7fZoA==", "dependencies": { "@photostructure/tz-lookup": "^8.0.0", - "@types/luxon": "^3.3.2", + "@types/luxon": "^3.3.5", "batch-cluster": "^12.1.0", "he": "^1.2.0", - "luxon": "^3.4.3" + "luxon": "^3.4.4" }, "optionalDependencies": { - "exiftool-vendored.exe": "12.67.0", - "exiftool-vendored.pl": "12.67.0" + "exiftool-vendored.exe": "12.70.0", + "exiftool-vendored.pl": "12.70.0" } }, - "node_modules/exiftool-vendored.pl": { - "version": "12.67.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.67.0.tgz", - "integrity": "sha512-Jvjkv4Cad+Bnp/4PuLEhO2BSpKy0MBccmq8if/H8V2ykssZrpUh8DRwEJkONnsaNX7dqKfObbOFig3vwoDyXsA==", + "node_modules/exiftool-vendored.exe": { + "version": "12.70.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.70.0.tgz", + "integrity": "sha512-Jp78fvWCls2q3+6P0GxEpgBEgIj2MJsrOKxfAnVzADO3BZ8jRTdYCHVMUoxbiuzIcqzLmQA2mPLjhQ35Y4MyeA==", + "optional": true, "os": [ - "!win32" + "win32" ] }, - "node_modules/exiftool-vendored/node_modules/exiftool-vendored.pl": { - "version": "12.67.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.67.0.tgz", - "integrity": "sha512-Jvjkv4Cad+Bnp/4PuLEhO2BSpKy0MBccmq8if/H8V2ykssZrpUh8DRwEJkONnsaNX7dqKfObbOFig3vwoDyXsA==", - "optional": true, + "node_modules/exiftool-vendored.pl": { + "version": "12.70.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.70.0.tgz", + "integrity": "sha512-nozVKVE7Leq2lQ+Kd6VbX+S04z0TKYGd42F1odwEl8AtWNlJViZ7rsi+OLHkJ0llWmObotviYAsj/xhF28qPSQ==", "os": [ "!win32" ] @@ -6293,41 +6278,6 @@ "bser": "2.1.1" } }, - "node_modules/fdir": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-5.3.0.tgz", - "integrity": "sha512-BtE53+jaa7nNHT+gPdfU6cFAXOJUWDs2b5GFox8dtl6zLXmfNf/N6im69b9nqNNwDyl27mpIWX8qR7AafWzSdQ==", - "peerDependencies": { - "picomatch": "2.x" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -6541,15 +6491,15 @@ } }, "node_modules/fork-ts-checker-webpack-plugin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", - "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", + "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", "dev": true, "dependencies": { "@babel/code-frame": "^7.16.7", "chalk": "^4.1.2", "chokidar": "^3.5.3", - "cosmiconfig": "^7.0.1", + "cosmiconfig": "^8.2.0", "deepmerge": "^4.2.2", "fs-extra": "^10.0.0", "memfs": "^3.4.1", @@ -6581,17 +6531,6 @@ "node": ">= 6" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/formidable": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", @@ -6804,6 +6743,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -7128,105 +7079,6 @@ "node": ">= 4" } }, - "node_modules/immich": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/immich/-/immich-0.41.0.tgz", - "integrity": "sha512-F+DIJ41/1L4Cqs0NqubcD2qttYcxTs32DCZS2l6U7d+a4SVtbxYwtEkvljSKACka1vokmHetpveGyRau2FdZ8w==", - "dependencies": { - "axios": "^0.26.0", - "chalk": "^2.4.1", - "cli-progress": "^3.10.0", - "commander": "^9.0.0", - "fdir": "^5.2.0", - "form-data": "^4.0.0", - "mime-types": "^2.1.34", - "p-limit": "3.1.0", - "systeminformation": "^5.11.6" - }, - "bin": { - "immich": "bin/index.js" - } - }, - "node_modules/immich/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/immich/node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, - "node_modules/immich/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/immich/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/immich/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/immich/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/immich/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/immich/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/immich/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -7293,7 +7145,6 @@ "version": "8.2.6", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", - "dev": true, "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", @@ -8329,9 +8180,9 @@ } }, "node_modules/joi": { - "version": "17.10.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.10.2.tgz", - "integrity": "sha512-hcVhjBxRNW/is3nNLdGLIjkgXetkeGc2wyhydhz8KumG23Aerk4HPjU5zaPAMRqXQFc0xNqXTC7+zQjxr0GlKA==", + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", + "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", "dependencies": { "@hapi/hoek": "^9.0.0", "@hapi/topo": "^5.0.0", @@ -8429,11 +8280,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/kdt": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/kdt/-/kdt-0.1.0.tgz", - "integrity": "sha512-ueX0gyv7tw4zBq9cQjaCr9qIhGTo5XYHUf/8aUUMHwoyb81KeCZHkSOoUwHGg/mgabvhTKCYjDUuYEmdak6Xjg==" - }, "node_modules/keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -8531,41 +8377,6 @@ "node": ">=6.11.5" } }, - "node_modules/local-reverse-geocoder": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/local-reverse-geocoder/-/local-reverse-geocoder-0.16.5.tgz", - "integrity": "sha512-MgJsyR3s8eeMfRfMvikwIdOG/jh9s78zPPX9kfx1qk5fwQLJnry5Qx5jreclqDPEpjOpNKIqz4aG5BbWGAGLbw==", - "hasInstallScript": true, - "dependencies": { - "async": "^3.2.4", - "csv-parse": "^5.5.0", - "debug": "^4.3.4", - "kdt": "^0.1.0", - "node-fetch": "^3.3.2", - "unzip-stream": "^0.3.1" - }, - "engines": { - "node": ">=11.0.0", - "npm": ">=6.4.1" - } - }, - "node_modules/local-reverse-geocoder/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8669,9 +8480,9 @@ } }, "node_modules/luxon": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", - "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", "engines": { "node": ">=12" } @@ -8927,6 +8738,12 @@ "node": ">=12.0.0" } }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", + "dev": true + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -9099,6 +8916,34 @@ "ncp": "bin/ncp" } }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dev": true, + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/nearley/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -9113,15 +8958,15 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "node_modules/nest-commander": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.12.0.tgz", - "integrity": "sha512-6ncAT13l7lH9Hya3GKKOIG+ltRD7b4idTlbuNXaCsm2IJIuuVxnx35UxiogJPz+GarE437H3I+GJXzehBnDQqg==", + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.12.2.tgz", + "integrity": "sha512-iOIVzwepP21fFfU629lxGeWABhDRXfOmJeNsuqmdonx/TEYUpWuPJCPYMKRKOpv/UQj0vf/8hjbzq25VfU+0xQ==", "dependencies": { "@fig/complete-commander": "^2.0.1", "@golevelup/nestjs-discovery": "4.0.0", "commander": "11.0.0", - "cosmiconfig": "8.2.0", - "inquirer": "8.2.5" + "cosmiconfig": "8.3.6", + "inquirer": "8.2.6" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", @@ -9137,7 +8982,7 @@ "prettier": "^2.3.2" }, "peerDependencies": { - "commander": "^11.0.0" + "commander": "^8.0.0" } }, "node_modules/nest-commander/node_modules/commander": { @@ -9148,48 +8993,6 @@ "node": ">=16" } }, - "node_modules/nest-commander/node_modules/cosmiconfig": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", - "dependencies": { - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - } - }, - "node_modules/nest-commander/node_modules/inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/nest-commander/node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -9204,22 +9007,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/nest-commander/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/node-abi": { "version": "3.47.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.47.0.tgz", @@ -9241,24 +9028,6 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -9444,11 +9213,11 @@ } }, "node_modules/openid-client": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.5.0.tgz", - "integrity": "sha512-Y7Xl8BgsrkzWLHkVDYuroM67hi96xITyEDSkmWaGUiNX6CkcXC3XyQGdv5aWZ6dukVKBFVQCADi9gCavOmU14w==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz", + "integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==", "dependencies": { - "jose": "^4.14.4", + "jose": "^4.15.1", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -9548,6 +9317,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -9822,7 +9592,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, + "dev": true, "engines": { "node": ">=8.6" }, @@ -10013,9 +9783,9 @@ } }, "node_modules/prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -10040,9 +9810,9 @@ } }, "node_modules/prettier-plugin-organize-imports": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.3.tgz", - "integrity": "sha512-KFvk8C/zGyvUaE3RvxN2MhCLwzV6OBbFSkwZ2OamCrs9ZY4i5L77jQ/w4UmUr+lqX8qbaqVq6bZZkApn+IgJSg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz", + "integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==", "dev": true, "peerDependencies": { "@volar/vue-language-plugin-pug": "^1.0.4", @@ -10243,6 +10013,25 @@ "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "dev": true + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -10501,6 +10290,15 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -11085,6 +10883,20 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sql-formatter": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-14.0.0.tgz", + "integrity": "sha512-VcHYMRvZqg3RNjjxNB/puT9O1hR5QLXTvgTaBtxXcvmRQwSnH9M+oW2Ti+uFuVVU8HoNlOjU2uKHv8c0FQNsdQ==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "get-stdin": "=8.0.0", + "nearley": "^2.20.1" + }, + "bin": { + "sql-formatter": "bin/sql-formatter-cli.cjs" + } + }, "node_modules/ssh-remote-port-forward": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz", @@ -11340,9 +11152,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.7.2.tgz", - "integrity": "sha512-mVZc9QVQ6pTCV5crli3+Ng+DoMPwdtMHK8QLk2oX8Mtamp4D/hV+uYdC3lV0JZrDgpNEcjs0RrWTqMwwosuLPQ==" + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.9.1.tgz", + "integrity": "sha512-5zAx+hUwJb9T3EAntc7TqYkV716CMqG6sZpNlAAMOMWkNXRYxGkN8ADIvD55dQZ10LxN90ZM/TQmN7y1gpICnw==" }, "node_modules/symbol-observable": { "version": "4.0.0", @@ -11369,31 +11181,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/systeminformation": { - "version": "5.21.9", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.9.tgz", - "integrity": "sha512-7pI4mu9P/2MGDV0T49B52E7IULBGj+kRVk6JSYUj5qfAk7N7C7aNX15fXziqrbgZntc6/jjYzWeb/x41jhg/eA==", - "os": [ - "darwin", - "linux", - "win32", - "freebsd", - "openbsd", - "netbsd", - "sunos", - "android" - ], - "bin": { - "systeminformation": "lib/cli.js" - }, - "engines": { - "node": ">=8.0.0" - }, - "funding": { - "type": "Buy me a coffee", - "url": "https://www.buymeacoffee.com/systeminfo" - } - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -11585,22 +11372,23 @@ } }, "node_modules/testcontainers": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.2.1.tgz", - "integrity": "sha512-R9LUMUEkKGSL2M4cP466Jah+Vi+ZLFlvrT4BENjEKJKNzubATOmDk26RHe8DHeFT+hnMD6fvVii+McXr0UTO7g==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.3.2.tgz", + "integrity": "sha512-IsV6NgS5reHcVF1nJLCDJv1hM9gAWUhLwh9b3ybgzvM3X7T2dcmuLFKt1RAR8qN8k+44tW2Drj7idxW6oeGvvg==", "dev": true, "dependencies": { "@balena/dockerignore": "^1.0.2", - "archiver": "^5.3.1", + "@types/dockerode": "^3.3.21", + "archiver": "^5.3.2", "async-lock": "^1.4.0", "byline": "^5.0.0", "debug": "^4.3.4", "docker-compose": "^0.24.2", "dockerode": "^3.3.5", "get-port": "^5.1.1", - "node-fetch": "^2.6.12", + "node-fetch": "^2.7.0", "proper-lockfile": "^4.1.2", - "properties-reader": "^2.2.0", + "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", "tar-fs": "^3.0.4", "tmp": "^0.2.1" @@ -11905,14 +11693,6 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", - "engines": { - "node": "*" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -11986,15 +11766,16 @@ } }, "node_modules/ts-loader": { - "version": "9.4.4", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.4.tgz", - "integrity": "sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", "dev": true, "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", "micromatch": "^4.0.0", - "semver": "^7.3.4" + "semver": "^7.3.4", + "source-map": "^0.7.4" }, "engines": { "node": ">=12.0.0" @@ -12382,9 +12163,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -12395,13 +12176,16 @@ } }, "node_modules/typesense": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/typesense/-/typesense-1.7.1.tgz", - "integrity": "sha512-RVtwprXDyU8MfAtLZ3PyH9WWRpEFGwij5BTu9I3VBjPIWtIvM/hbi2ogL/UK9dPXFECaxY8HlHrZz9djhJDZtg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/typesense/-/typesense-1.7.2.tgz", + "integrity": "sha512-hgQESOiyNJq+w2mpRJa/a1UMhWtJ/+sb0p7NoeCDSkikm9sasisJdnc7uhQchM6vTWKw2sMLWUBNbAhItR6zUQ==", "dependencies": { "axios": "^0.26.0", "loglevel": "^1.8.0" }, + "engines": { + "node": ">=14" + }, "peerDependencies": { "@babel/runtime": "^7.17.2" } @@ -12415,9 +12199,9 @@ } }, "node_modules/ua-parser-js": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz", - "integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==", + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", + "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", "funding": [ { "type": "opencollective", @@ -12467,6 +12251,11 @@ "node": ">= 4.0.0" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -12493,15 +12282,6 @@ "node": ">=8" } }, - "node_modules/unzip-stream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.1.tgz", - "integrity": "sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==", - "dependencies": { - "binary": "^0.3.0", - "mkdirp": "^0.5.1" - } - }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -12659,23 +12439,15 @@ "defaults": "^1.0.3" } }, - "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.88.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", - "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -12867,7 +12639,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -12961,12 +12732,11 @@ "dev": true }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yargs": { @@ -13015,6 +12785,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, "engines": { "node": ">=10" }, @@ -13054,9 +12825,9 @@ } }, "@angular-devkit/core": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.3.tgz", - "integrity": "sha512-oZLdg2XTx7likYAXRj1CU0XmrsCfe5f2grj3iwuI3OB1LXwwpdbHBztruj03y3yHES+TnO+dIbkvRnvMXs7uAA==", + "version": "16.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.8.tgz", + "integrity": "sha512-PTGozYvh1Bin5lB15PwcXa26Ayd17bWGLS3H8Rs0s+04mUDvfNofmweaX1LgumWWy3nCUTDuwHxX10M3G0wE2g==", "dev": true, "requires": { "ajv": "8.12.0", @@ -13068,12 +12839,12 @@ } }, "@angular-devkit/schematics": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.3.tgz", - "integrity": "sha512-+lBiHxi/C9HCfiCbtW25DldwvJDXXXv5oWw+Tg4s18BO/lYZLveGUEaZWu9ZJ5VIJ8GliUi2LohxhDxBkh4Oxg==", + "version": "16.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.8.tgz", + "integrity": "sha512-MBiKZOlR9/YMdflALr7/7w/BGAfo/BGTrlkqsIB6rDWV1dYiCgxI+033HsiNssLS6RQyCFx/e7JA2aBBzu9zEg==", "dev": true, "requires": { - "@angular-devkit/core": "16.2.3", + "@angular-devkit/core": "16.2.8", "jsonc-parser": "3.2.0", "magic-string": "0.30.1", "ora": "5.4.1", @@ -13081,13 +12852,13 @@ } }, "@angular-devkit/schematics-cli": { - "version": "16.2.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-16.2.3.tgz", - "integrity": "sha512-5YQCbQmY9Kc03a9Io4XHOrxGXjnzcVveUuUO64R1m5x2aA5I+mVR8NVvxuoGRAeoI1FWusAKRe9hH8nRCLrelA==", + "version": "16.2.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-16.2.8.tgz", + "integrity": "sha512-EXURJCzWTVYCipiTT4vxQQOrF63asOUDbeOy3OtiSh7EwIUvxm3BPG6hquJqngEnI/N6bA75NJ1fBhU6Hrh7eA==", "dev": true, "requires": { - "@angular-devkit/core": "16.2.3", - "@angular-devkit/schematics": "16.2.3", + "@angular-devkit/core": "16.2.8", + "@angular-devkit/schematics": "16.2.8", "ansi-colors": "4.1.3", "inquirer": "8.2.4", "symbol-observable": "4.0.0", @@ -13553,9 +13324,9 @@ } }, "@babel/runtime": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", - "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", + "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==", "requires": { "regenerator-runtime": "^0.14.0" } @@ -13664,9 +13435,9 @@ "dev": true }, "@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -13701,9 +13472,9 @@ } }, "@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "dev": true }, "@golevelup/nestjs-discovery": { @@ -13728,12 +13499,12 @@ } }, "@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" } @@ -13745,11 +13516,32 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "@immich/cli": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@immich/cli/-/cli-2.0.4.tgz", + "integrity": "sha512-Fzi4sESi1pD0YdqhDAlGgR8WfeBEsfxuNpbzgY6DklmgTeIDOh29pv+ESo080uWfKhXdBBhYgc1ZUc6m5krxQw==", + "requires": { + "axios": "^1.6.2", + "byte-size": "^8.1.1", + "cli-progress": "^3.12.0", + "commander": "^11.0.0", + "form-data": "^4.0.0", + "glob": "^10.3.1", + "yaml": "^2.3.1" + }, + "dependencies": { + "commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" + } + } + }, "@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -14249,20 +14041,21 @@ } }, "@nestjs/cli": { - "version": "10.1.18", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.1.18.tgz", - "integrity": "sha512-jQtG47keLsACt7b4YwJbTBYRm90n82gJpMaiR1HGAyQ9pccbctjSYu592eT4bxqkUWxPgBE3mpNynXj7dWAfrw==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.2.1.tgz", + "integrity": "sha512-CAJAQwmxFZfB3RTvqz/eaXXWpyU+mZ4QSqfBYzjneTsPgF+uyOAW3yQpaLNn9Dfcv39R9UxSuAhayv6yuFd+Jg==", "dev": true, "requires": { - "@angular-devkit/core": "16.2.3", - "@angular-devkit/schematics": "16.2.3", - "@angular-devkit/schematics-cli": "16.2.3", + "@angular-devkit/core": "16.2.8", + "@angular-devkit/schematics": "16.2.8", + "@angular-devkit/schematics-cli": "16.2.8", "@nestjs/schematics": "^10.0.1", "chalk": "4.1.2", "chokidar": "3.5.3", "cli-table3": "0.6.3", "commander": "4.1.1", - "fork-ts-checker-webpack-plugin": "8.0.0", + "fork-ts-checker-webpack-plugin": "9.0.2", + "glob": "10.3.10", "inquirer": "8.2.6", "node-emoji": "1.11.0", "ora": "5.4.1", @@ -14274,7 +14067,7 @@ "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.2.2", - "webpack": "5.88.2", + "webpack": "5.89.0", "webpack-node-externals": "3.0.0" }, "dependencies": { @@ -14287,18 +14080,6 @@ "balanced-match": "^1.0.0" } }, - "glob": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", - "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "minimatch": "^8.0.2", - "minipass": "^4.2.4", - "path-scurry": "^1.6.1" - } - }, "minimatch": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", @@ -14321,14 +14102,34 @@ "dev": true, "requires": { "glob": "^9.2.0" + }, + "dependencies": { + "glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + } + } } + }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true } } }, "@nestjs/common": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.6.tgz", - "integrity": "sha512-ma8R7n+FXsWM4XF9QXjjrsRceyRzid/xKmNKVOa/sTJntkVG8lL71BHBEfjtFvO6EJUqjs/15LbDc0iaN5nCwA==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.10.tgz", + "integrity": "sha512-fwAk931rjW8CNH2Mgwawq/7HWHH1dxkOLdcgs7U52ddLk8CtHXjejm1cbNahewlSbNhvlOl7y1STLHutE6sUqw==", "requires": { "iterare": "1.2.1", "tslib": "2.6.2", @@ -14354,9 +14155,9 @@ } }, "@nestjs/core": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.6.tgz", - "integrity": "sha512-oGQ2CoBeFRT7egG47MFqS89xlXBTIRZBkRpKRTPMftEfL1RMXhXIcIIaGfzp11wx6qxrBVxBXpVLM09oaqHpaQ==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.10.tgz", + "integrity": "sha512-+ckOI6BPi2ZMHikT9MCG4ctHDc4OnjhoIytrn7f2AYMMXI4bnutJhqyQKc30VDka5x3Wq6QAD57pgSP7y+JjJg==", "requires": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", @@ -14374,15 +14175,15 @@ } }, "@nestjs/mapped-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.2.tgz", - "integrity": "sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.3.tgz", + "integrity": "sha512-40Zdqg98lqoF0+7ThWIZFStxgzisK6GG22+1ABO4kZiGF/Tu2FE+DYLw+Q9D94vcFWizJ+MSjNN4ns9r6hIGxw==", "requires": {} }, "@nestjs/platform-express": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.6.tgz", - "integrity": "sha512-4U16D5ot2570CR8Qm5qu/SBXsA2l5KxN7AVSGvzoWoBxjEoOnnZOapC5Pler3yYa0tT1xLhji61RX1gceKW3dw==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.10.tgz", + "integrity": "sha512-U4KDgtMjH8TqEvt0RzC/POP8ABvL9bYoCScvlGtFSKgVGaMLBKkZ4+jHtbQx6qItYSlBBRUuz/dveMZCObfrkQ==", "requires": { "body-parser": "1.20.2", "cors": "2.8.5", @@ -14399,9 +14200,9 @@ } }, "@nestjs/platform-socket.io": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.2.6.tgz", - "integrity": "sha512-c4GbHeyd12hyrLngnHDouBui0fwPjaK4TopkZdvkskRCd4sOfph9EUX2CHny+ZL8UeY8IbW/yBZxQ746ahSYsQ==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.2.10.tgz", + "integrity": "sha512-JBuemeIBp2mpp+z7D12oa22k83TnDTxyQDMKZpO/B2/QnBVR+2C4EZ07/XOct14LQXO6vIjmT0iYYCZbNvczjw==", "requires": { "socket.io": "4.7.2", "tslib": "2.6.2" @@ -14431,71 +14232,34 @@ } }, "@nestjs/schematics": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.0.2.tgz", - "integrity": "sha512-DaZZjymYoIfRqC5W62lnYXIIods1PDY6CGc8+IpRwyinzffjKxZ3DF3exu+mdyvllzkXo9DTXkoX4zOPSJHCkw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.0.3.tgz", + "integrity": "sha512-2BRujK0GqGQ7j1Zpz+obVfskDnnOeVKt5aXoSaVngKo8Oczy8uYCY+R547TQB+Kf35epdfFER2pVnQrX3/It5A==", "dev": true, "requires": { - "@angular-devkit/core": "16.1.8", - "@angular-devkit/schematics": "16.1.8", + "@angular-devkit/core": "16.2.8", + "@angular-devkit/schematics": "16.2.8", "comment-json": "4.2.3", "jsonc-parser": "3.2.0", "pluralize": "8.0.0" - }, - "dependencies": { - "@angular-devkit/core": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.8.tgz", - "integrity": "sha512-dSRD/+bGanArIXkj+kaU1kDFleZeQMzmBiOXX+pK0Ah9/0Yn1VmY3RZh1zcX9vgIQXV+t7UPrTpOjaERMUtVGw==", - "dev": true, - "requires": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "7.8.1", - "source-map": "0.7.4" - } - }, - "@angular-devkit/schematics": { - "version": "16.1.8", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.8.tgz", - "integrity": "sha512-6LyzMdFJs337RTxxkI2U1Ndw0CW5mMX/aXWl8d7cW2odiSrAg8IdlMqpc+AM8+CPfsB0FtS1aWkEZqJLT0jHOg==", - "dev": true, - "requires": { - "@angular-devkit/core": "16.1.8", - "jsonc-parser": "3.2.0", - "magic-string": "0.30.0", - "ora": "5.4.1", - "rxjs": "7.8.1" - } - }, - "magic-string": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", - "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.13" - } - } } }, "@nestjs/swagger": { - "version": "7.1.12", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.12.tgz", - "integrity": "sha512-Q1P/IE+cws0sJeNtbs+8uDalcVylpmAnaEUFenGOa3KSNnXF/8DOE84mET/uUhFXsiz9PLHK8Hy7o7B6fRpMhg==", + "version": "7.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.16.tgz", + "integrity": "sha512-f9KBk/BX9MUKPTj7tQNYJ124wV/jP5W2lwWHLGwe/4qQXixuDOo39zP55HIJ44LE7S04B7BOeUOo9GBJD/vRcw==", "requires": { - "@nestjs/mapped-types": "2.0.2", + "@nestjs/mapped-types": "2.0.3", "js-yaml": "4.1.0", "lodash": "4.17.21", "path-to-regexp": "3.2.0", - "swagger-ui-dist": "5.7.2" + "swagger-ui-dist": "5.9.1" } }, "@nestjs/testing": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.6.tgz", - "integrity": "sha512-uxlxHhpSvG4yDTPmuPneoQL1/UnBkOkzE+Zaz6bwURg7lc3uS4ZsXl75OL3pYaJH37rHYXYT9bGcYSpxVbwIrg==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.10.tgz", + "integrity": "sha512-IVLUnPz/+fkBtPATYfqTIP+phN9yjkXejmj+JyhmcfPJZpxBmD1i9VSMqa4u54l37j0xkGPscQ0IXpbhqMYUKw==", "dev": true, "requires": { "tslib": "2.6.2" @@ -14510,17 +14274,24 @@ } }, "@nestjs/typeorm": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.0.tgz", - "integrity": "sha512-WQU4HCDTz4UavsFzvGUKDHqi0MO5K47yFoPXdmh+Z/hCNO7SHCMmV9jLiLukM8n5nKUqJ3jDqiljkWBcZPdCtA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.1.tgz", + "integrity": "sha512-YVFYL7D25VAVp5/G+KLXIgsRfYomA+VaFZBpm2rtwrrBOmkXNrxr7kuI2bBBO/Xy4kKBDe6wbvIVVFeEA7/ngA==", "requires": { - "uuid": "9.0.0" + "uuid": "9.0.1" + }, + "dependencies": { + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + } } }, "@nestjs/websockets": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.2.6.tgz", - "integrity": "sha512-HwZADfixAMKMdMB/eBz0HJnPCs0r+W+5inpRwCazsQhwZniGUgXkfIhyRvNfHip/nb+DLS/M8BNBR2JGiJNTEg==", + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.2.10.tgz", + "integrity": "sha512-L1AkxwLUj/ntk26jO1SXYl3GRElQE6Fikzfy/3MPFURk0GDs7tHUzLcb8BC8q8u5ZpUjBAC2wFVQzrY5R0MHNw==", "requires": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -14944,12 +14715,12 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "@testcontainers/postgresql": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.2.1.tgz", - "integrity": "sha512-snIB11wyHUYPzQNNgoHpiRcZO3sR0mDwU28cwOd0lAT4kzTvTihZTmz4gv8MYppcXlRsLAIE0QqzGauZsfFQiA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.3.2.tgz", + "integrity": "sha512-PhbmhWe+M5RF9QZGOzg/Vc+Ve6KlT/j9OLv9G11xubvcdhbuAz9Am3u1GjsXS7C1Vt/rwWx+j0kg+FtYwbJQng==", "dev": true, "requires": { - "testcontainers": "^10.2.1" + "testcontainers": "^10.3.2" } }, "@tsconfig/node10": { @@ -14999,9 +14770,9 @@ } }, "@types/archiver": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.3.tgz", - "integrity": "sha512-0ABdVcXL6jOwNGY+hjWPqrxUvKelBEwNLcuv/SV2vZ4YCH8w9NttFCt+/QqI5zgMX+iX/XqVy89/r7EmLJmMpQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-6.0.2.tgz", + "integrity": "sha512-KmROQqbQzKGuaAbmK+ZcytkJ51+YqDa7NmbXjmtC5YBLSyQYo21YaUnQ3HbaPFKL1ooo6RQ6OPYPIDyxfpDDXw==", "dev": true, "requires": { "@types/readdir-glob": "*" @@ -15049,9 +14820,9 @@ } }, "@types/bcrypt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz", - "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", "dev": true, "requires": { "@types/node": "*" @@ -15082,9 +14853,9 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "@types/cookie-parser": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.4.tgz", - "integrity": "sha512-Var+aj5I6ZgIqsQ05N2V8q5OBrFfZXtIGWWDSrEYLIbMw758obagSwdGcLCjwh1Ga7M7+wj0SDIAaAC/WT7aaA==", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==", "dev": true, "requires": { "@types/express": "*" @@ -15113,6 +14884,26 @@ "cron": "*" } }, + "@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "@types/dockerode": { + "version": "3.3.23", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.23.tgz", + "integrity": "sha512-Lz5J+NFgZS4cEVhquwjIGH4oQwlVn2h7LXD3boitujBnzOE5o7s9H8hchEjoDK2SlRsJTogdKnQeiJgPPKLIEw==", + "dev": true, + "requires": { + "@types/docker-modem": "*", + "@types/node": "*" + } + }, "@types/eslint": { "version": "8.44.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", @@ -15140,9 +14931,9 @@ "dev": true }, "@types/express": { - "version": "4.17.18", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz", - "integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dev": true, "requires": { "@types/body-parser": "*", @@ -15164,9 +14955,9 @@ } }, "@types/fluent-ffmpeg": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.22.tgz", - "integrity": "sha512-ZZPDDrDOb2Ahp5fxZzuw64f0rCcviv+SDuCyJ1PIF/UFn9wNHtb/bY8Dj/2nrbQ7SNsGI7gaO2wJVkkU2HBcMg==", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.24.tgz", + "integrity": "sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==", "dev": true, "requires": { "@types/node": "*" @@ -15188,9 +14979,9 @@ "dev": true }, "@types/imagemin": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.1.tgz", - "integrity": "sha512-DSpM//dRPzme7doePGkmR1uoquHi0h0ElaA5qFnxHECfFcB8z/jhMI8eqmxWNpHn9ZG18p4PC918sZLhR0cr5A==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.5.tgz", + "integrity": "sha512-tah3dm+5sG+fEDAz6CrQ5evuEaPX9K6DF3E5a01MPOKhA2oGBoC+oA5EJzSugB905sN4DE19EDzldT2Cld2g6Q==", "dev": true, "requires": { "@types/node": "*" @@ -15231,9 +15022,9 @@ } }, "@types/jest": { - "version": "29.5.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", - "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", + "version": "29.5.10", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", + "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", "dev": true, "requires": { "expect": "^29.0.0", @@ -15241,9 +15032,9 @@ } }, "@types/jest-when": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@types/jest-when/-/jest-when-3.5.3.tgz", - "integrity": "sha512-gJWLSCRWEkPHYXYjjrxEMfWopoA2bl9nUbwDomxIzRXfe0gLkmSDPD0lLcoAiO2RDpXDoHSeigy/rNa/ja0MfQ==", + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@types/jest-when/-/jest-when-3.5.5.tgz", + "integrity": "sha512-H9MDPIrz7NOu6IXP9OHExNN9LnJbGYAzRsGIDKxWr7Fth9vovemNV8yFbkUWLSEmuA8PREvAEvt9yK0PPLmFHA==", "dev": true, "requires": { "@types/jest": "*" @@ -15256,15 +15047,15 @@ "dev": true }, "@types/lodash": { - "version": "4.14.199", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", - "integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==", + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", "dev": true }, "@types/luxon": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz", - "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==" + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.5.tgz", + "integrity": "sha512-1cyf6Ge/94zlaWIZA2ei1pE6SZ8xpad2hXaYa5JEFiaUH0YS494CZwyi4MXNpXD9oEuv6ZH0Bmh0e7F9sPhmZA==" }, "@types/mime": { "version": "1.3.3", @@ -15273,39 +15064,36 @@ "dev": true }, "@types/mock-fs": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.2.tgz", - "integrity": "sha512-mSIMAOjrNTVUFmZgJEigSIm+GlS4hbrk8U5+M8EB45uMrykKdN9TidjjSaOY1yFph2+TD7bsIfB4r+IrMYVyPQ==", + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz", + "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==", "dev": true, "requires": { "@types/node": "*" } }, "@types/multer": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.8.tgz", - "integrity": "sha512-VMZOW6mnmMMhA5m3fsCdXBwFwC+a+27/8gctNMuQC4f7UtWcF79KAFGoIfKZ4iqrElgWIa3j5vhMJDp0iikQ1g==", + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", "dev": true, "requires": { "@types/express": "*" } }, "@types/mv": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@types/mv/-/mv-2.1.2.tgz", - "integrity": "sha512-IvAjPuiQ2exDicnTrMidt1m+tj3gZ60BM0PaoRsU0m9Cn+lrOyemuO9Tf8CvHFmXlxMjr1TVCfadi9sfwbSuKg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mv/-/mv-2.1.4.tgz", + "integrity": "sha512-MgEHBpXnQo44Q43j8G0Bvp/Yi8LYbC8hxKrRFMgDEDZMmzDKZLgiyMWtW49B37ko+QupgZ3G5rtPUnOGe5ixLw==", "dev": true }, "@types/node": { - "version": "20.8.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", - "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==" - }, - "@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true + "version": "20.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", + "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", + "requires": { + "undici-types": "~5.26.4" + } }, "@types/qs": { "version": "6.9.8", @@ -15329,9 +15117,9 @@ } }, "@types/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "@types/send": { @@ -15400,9 +15188,9 @@ } }, "@types/supertest": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.13.tgz", - "integrity": "sha512-Vc/5/pRwSC055fU7Wu8erTj4gLpID9SdG2zRMuqaHLni3GTsrJ8gyB6MbFZZGLW6vQaGPhiUWRB6uWglv87MEg==", + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.16.tgz", + "integrity": "sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==", "dev": true, "requires": { "@types/superagent": "*" @@ -15418,9 +15206,9 @@ } }, "@types/ua-parser-js": { - "version": "0.7.37", - "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.37.tgz", - "integrity": "sha512-4sOxS3ZWXC0uHJLYcWAaLMxTvjRX3hT96eF4YWUh1ovTaenvibaZOE5uXtIp4mksKMLRwo7YDiCBCw6vBiUPVg==", + "version": "0.7.39", + "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz", + "integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==", "dev": true }, "@types/validator": { @@ -15444,16 +15232,16 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz", - "integrity": "sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", + "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/type-utils": "6.7.4", - "@typescript-eslint/utils": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/type-utils": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -15463,54 +15251,54 @@ } }, "@typescript-eslint/parser": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.4.tgz", - "integrity": "sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/typescript-estree": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz", - "integrity": "sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", "dev": true, "requires": { - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4" + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" } }, "@typescript-eslint/type-utils": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.4.tgz", - "integrity": "sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", + "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "6.7.4", - "@typescript-eslint/utils": "6.7.4", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/utils": "6.13.1", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.4.tgz", - "integrity": "sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz", - "integrity": "sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", "dev": true, "requires": { - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -15519,30 +15307,36 @@ } }, "@typescript-eslint/utils": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.4.tgz", - "integrity": "sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/typescript-estree": "6.7.4", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", "semver": "^7.5.4" } }, "@typescript-eslint/visitor-keys": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz", - "integrity": "sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", "dev": true, "requires": { - "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/types": "6.13.1", "eslint-visitor-keys": "^3.4.1" } }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -15965,9 +15759,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -16114,15 +15908,6 @@ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", "dev": true }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -16254,11 +16039,6 @@ "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==" - }, "buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -16267,9 +16047,9 @@ "optional": true }, "bullmq": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.12.0.tgz", - "integrity": "sha512-eAN7WnlR6MszikwQXE16oGMUWngfbYG0SqkxiwUmVm4muR3KYg+etIRZE3vRKNXWKw0WxJZTA+3oBfDMcCBh+w==", + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.4.tgz", + "integrity": "sha512-8tD3Zq4CP+2q+zZ1JSkKov5ra18Jhth8zdr2X1/V2MMOVpa8vfVCsQaRnKgDpiMpaF6AH9sucXUNtk4xamTEKw==", "requires": { "cron-parser": "^4.6.0", "glob": "^8.0.3", @@ -16335,6 +16115,11 @@ "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", "dev": true }, + "byte-size": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", + "integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==" + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -16366,14 +16151,6 @@ "integrity": "sha512-UrtAXVcj1mvPBFQ4sKd38daP8dEcXXr5sQe6QNNinaPd0iA/cxg9/l3VrSdL73jgw5sKyuQ6jNgiKO12W3SsVA==", "dev": true }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -16763,16 +16540,14 @@ } }, "cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" } }, "cpu-features": { @@ -16845,11 +16620,6 @@ "luxon": "^3.2.1" } }, - "cron-validator": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cron-validator/-/cron-validator-1.3.1.tgz", - "integrity": "sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A==" - }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -16860,16 +16630,6 @@ "which": "^2.0.1" } }, - "csv-parse": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.0.tgz", - "integrity": "sha512-RxruSK3M4XgzcD7Trm2wEN+SJ26ChIb903+IWxNOcB5q4jT2Cs+hFr6QP39J05EohshRFEvyzEBoZ/466S2sbw==" - }, - "data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" - }, "date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -17099,6 +16859,12 @@ "path-type": "^4.0.0" } }, + "discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", + "dev": true + }, "docker-compose": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.2.tgz", @@ -17106,14 +16872,6 @@ "dev": true, "requires": { "yaml": "^2.2.2" - }, - "dependencies": { - "yaml": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", - "dev": true - } } }, "docker-modem": { @@ -17303,18 +17061,19 @@ "dev": true }, "eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.54.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -17384,9 +17143,9 @@ "requires": {} }, "eslint-plugin-prettier": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", - "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", + "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", "dev": true, "requires": { "prettier-linter-helpers": "^1.0.0", @@ -17493,31 +17252,29 @@ } }, "exiftool-vendored": { - "version": "23.1.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-23.1.0.tgz", - "integrity": "sha512-sZ1OUpvAWbUCCoidMMKDTTJ3hHE3mHxb4ihWKmta/eQYYMR54Mssp6+Nf7HoFvY//nX5YK2VCOGVexGGuhM8Bw==", + "version": "23.5.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-23.5.0.tgz", + "integrity": "sha512-6wlCNFFOcOcRa8GpuCzg0dhOcRMipONMZidP3jYUF0g7YkhknKxG4aA0BdmeH2Tp3Hm0286egicslGW9d7fZoA==", "requires": { "@photostructure/tz-lookup": "^8.0.0", - "@types/luxon": "^3.3.2", + "@types/luxon": "^3.3.5", "batch-cluster": "^12.1.0", - "exiftool-vendored.exe": "12.67.0", - "exiftool-vendored.pl": "12.67.0", + "exiftool-vendored.exe": "12.70.0", + "exiftool-vendored.pl": "12.70.0", "he": "^1.2.0", - "luxon": "^3.4.3" - }, - "dependencies": { - "exiftool-vendored.pl": { - "version": "12.67.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.67.0.tgz", - "integrity": "sha512-Jvjkv4Cad+Bnp/4PuLEhO2BSpKy0MBccmq8if/H8V2ykssZrpUh8DRwEJkONnsaNX7dqKfObbOFig3vwoDyXsA==", - "optional": true - } + "luxon": "^3.4.4" } }, + "exiftool-vendored.exe": { + "version": "12.70.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.70.0.tgz", + "integrity": "sha512-Jp78fvWCls2q3+6P0GxEpgBEgIj2MJsrOKxfAnVzADO3BZ8jRTdYCHVMUoxbiuzIcqzLmQA2mPLjhQ35Y4MyeA==", + "optional": true + }, "exiftool-vendored.pl": { - "version": "12.67.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.67.0.tgz", - "integrity": "sha512-Jvjkv4Cad+Bnp/4PuLEhO2BSpKy0MBccmq8if/H8V2ykssZrpUh8DRwEJkONnsaNX7dqKfObbOFig3vwoDyXsA==" + "version": "12.70.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.70.0.tgz", + "integrity": "sha512-nozVKVE7Leq2lQ+Kd6VbX+S04z0TKYGd42F1odwEl8AtWNlJViZ7rsi+OLHkJ0llWmObotviYAsj/xhF28qPSQ==" }, "exit": { "version": "0.1.2", @@ -17711,21 +17468,6 @@ "bser": "2.1.1" } }, - "fdir": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-5.3.0.tgz", - "integrity": "sha512-BtE53+jaa7nNHT+gPdfU6cFAXOJUWDs2b5GFox8dtl6zLXmfNf/N6im69b9nqNNwDyl27mpIWX8qR7AafWzSdQ==", - "requires": {} - }, - "fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "requires": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - } - }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -17882,15 +17624,15 @@ } }, "fork-ts-checker-webpack-plugin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", - "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", + "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", "dev": true, "requires": { "@babel/code-frame": "^7.16.7", "chalk": "^4.1.2", "chokidar": "^3.5.3", - "cosmiconfig": "^7.0.1", + "cosmiconfig": "^8.2.0", "deepmerge": "^4.2.2", "fs-extra": "^10.0.0", "memfs": "^3.4.1", @@ -17911,14 +17653,6 @@ "mime-types": "^2.1.12" } }, - "formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "requires": { - "fetch-blob": "^3.1.2" - } - }, "formidable": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", @@ -18081,6 +17815,12 @@ "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", "dev": true }, + "get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "dev": true + }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -18300,86 +18040,6 @@ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", "dev": true }, - "immich": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/immich/-/immich-0.41.0.tgz", - "integrity": "sha512-F+DIJ41/1L4Cqs0NqubcD2qttYcxTs32DCZS2l6U7d+a4SVtbxYwtEkvljSKACka1vokmHetpveGyRau2FdZ8w==", - "requires": { - "axios": "^0.26.0", - "chalk": "^2.4.1", - "cli-progress": "^3.10.0", - "commander": "^9.0.0", - "fdir": "^5.2.0", - "form-data": "^4.0.0", - "mime-types": "^2.1.34", - "p-limit": "3.1.0", - "systeminformation": "^5.11.6" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "requires": { - "follow-redirects": "^1.14.8" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -18428,7 +18088,6 @@ "version": "8.2.6", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", - "dev": true, "requires": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", @@ -19200,9 +18859,9 @@ } }, "joi": { - "version": "17.10.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.10.2.tgz", - "integrity": "sha512-hcVhjBxRNW/is3nNLdGLIjkgXetkeGc2wyhydhz8KumG23Aerk4HPjU5zaPAMRqXQFc0xNqXTC7+zQjxr0GlKA==", + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", + "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", "requires": { "@hapi/hoek": "^9.0.0", "@hapi/topo": "^5.0.0", @@ -19280,11 +18939,6 @@ "universalify": "^2.0.0" } }, - "kdt": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/kdt/-/kdt-0.1.0.tgz", - "integrity": "sha512-ueX0gyv7tw4zBq9cQjaCr9qIhGTo5XYHUf/8aUUMHwoyb81KeCZHkSOoUwHGg/mgabvhTKCYjDUuYEmdak6Xjg==" - }, "keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -19369,31 +19023,6 @@ "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true }, - "local-reverse-geocoder": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/local-reverse-geocoder/-/local-reverse-geocoder-0.16.5.tgz", - "integrity": "sha512-MgJsyR3s8eeMfRfMvikwIdOG/jh9s78zPPX9kfx1qk5fwQLJnry5Qx5jreclqDPEpjOpNKIqz4aG5BbWGAGLbw==", - "requires": { - "async": "^3.2.4", - "csv-parse": "^5.5.0", - "debug": "^4.3.4", - "kdt": "^0.1.0", - "node-fetch": "^3.3.2", - "unzip-stream": "^0.3.1" - }, - "dependencies": { - "node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "requires": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - } - } - } - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -19478,9 +19107,9 @@ } }, "luxon": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", - "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==" + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" }, "macos-release": { "version": "2.5.1", @@ -19662,6 +19291,12 @@ "integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==", "dev": true }, + "moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -19814,6 +19449,26 @@ "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==" }, + "nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dev": true, + "requires": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -19825,15 +19480,15 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "nest-commander": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.12.0.tgz", - "integrity": "sha512-6ncAT13l7lH9Hya3GKKOIG+ltRD7b4idTlbuNXaCsm2IJIuuVxnx35UxiogJPz+GarE437H3I+GJXzehBnDQqg==", + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.12.2.tgz", + "integrity": "sha512-iOIVzwepP21fFfU629lxGeWABhDRXfOmJeNsuqmdonx/TEYUpWuPJCPYMKRKOpv/UQj0vf/8hjbzq25VfU+0xQ==", "requires": { "@fig/complete-commander": "^2.0.1", "@golevelup/nestjs-discovery": "4.0.0", "commander": "11.0.0", - "cosmiconfig": "8.2.0", - "inquirer": "8.2.5" + "cosmiconfig": "8.3.6", + "inquirer": "8.2.6" }, "dependencies": { "@fig/complete-commander": { @@ -19849,53 +19504,10 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==" }, - "cosmiconfig": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", - "requires": { - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0" - } - }, - "inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^7.0.0" - } - }, "prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==" - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } } } }, @@ -19917,11 +19529,6 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, - "node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" - }, "node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -20052,11 +19659,11 @@ } }, "openid-client": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.5.0.tgz", - "integrity": "sha512-Y7Xl8BgsrkzWLHkVDYuroM67hi96xITyEDSkmWaGUiNX6CkcXC3XyQGdv5aWZ6dukVKBFVQCADi9gCavOmU14w==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz", + "integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==", "requires": { - "jose": "^4.14.4", + "jose": "^4.15.1", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -20131,6 +19738,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "requires": { "yocto-queue": "^0.1.0" } @@ -20337,7 +19945,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true + "dev": true }, "pirates": { "version": "4.0.6", @@ -20478,9 +20086,9 @@ "dev": true }, "prettier": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", - "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true }, "prettier-linter-helpers": { @@ -20493,9 +20101,9 @@ } }, "prettier-plugin-organize-imports": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.3.tgz", - "integrity": "sha512-KFvk8C/zGyvUaE3RvxN2MhCLwzV6OBbFSkwZ2OamCrs9ZY4i5L77jQ/w4UmUr+lqX8qbaqVq6bZZkApn+IgJSg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz", + "integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==", "dev": true, "requires": {} }, @@ -20628,6 +20236,22 @@ "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" }, + "railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "dev": true + }, + "randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dev": true, + "requires": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + } + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -20834,6 +20458,12 @@ } } }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -21268,6 +20898,17 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "sql-formatter": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-14.0.0.tgz", + "integrity": "sha512-VcHYMRvZqg3RNjjxNB/puT9O1hR5QLXTvgTaBtxXcvmRQwSnH9M+oW2Ti+uFuVVU8HoNlOjU2uKHv8c0FQNsdQ==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "get-stdin": "=8.0.0", + "nearley": "^2.20.1" + } + }, "ssh-remote-port-forward": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz", @@ -21459,9 +21100,9 @@ "dev": true }, "swagger-ui-dist": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.7.2.tgz", - "integrity": "sha512-mVZc9QVQ6pTCV5crli3+Ng+DoMPwdtMHK8QLk2oX8Mtamp4D/hV+uYdC3lV0JZrDgpNEcjs0RrWTqMwwosuLPQ==" + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.9.1.tgz", + "integrity": "sha512-5zAx+hUwJb9T3EAntc7TqYkV716CMqG6sZpNlAAMOMWkNXRYxGkN8ADIvD55dQZ10LxN90ZM/TQmN7y1gpICnw==" }, "symbol-observable": { "version": "4.0.0", @@ -21479,11 +21120,6 @@ "tslib": "^2.5.0" } }, - "systeminformation": { - "version": "5.21.9", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.9.tgz", - "integrity": "sha512-7pI4mu9P/2MGDV0T49B52E7IULBGj+kRVk6JSYUj5qfAk7N7C7aNX15fXziqrbgZntc6/jjYzWeb/x41jhg/eA==" - }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -21623,22 +21259,23 @@ } }, "testcontainers": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.2.1.tgz", - "integrity": "sha512-R9LUMUEkKGSL2M4cP466Jah+Vi+ZLFlvrT4BENjEKJKNzubATOmDk26RHe8DHeFT+hnMD6fvVii+McXr0UTO7g==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.3.2.tgz", + "integrity": "sha512-IsV6NgS5reHcVF1nJLCDJv1hM9gAWUhLwh9b3ybgzvM3X7T2dcmuLFKt1RAR8qN8k+44tW2Drj7idxW6oeGvvg==", "dev": true, "requires": { "@balena/dockerignore": "^1.0.2", - "archiver": "^5.3.1", + "@types/dockerode": "^3.3.21", + "archiver": "^5.3.2", "async-lock": "^1.4.0", "byline": "^5.0.0", "debug": "^4.3.4", "docker-compose": "^0.24.2", "dockerode": "^3.3.5", "get-port": "^5.1.1", - "node-fetch": "^2.6.12", + "node-fetch": "^2.7.0", "proper-lockfile": "^4.1.2", - "properties-reader": "^2.2.0", + "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", "tar-fs": "^3.0.4", "tmp": "^0.2.1" @@ -21891,11 +21528,6 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==" - }, "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -21934,15 +21566,16 @@ } }, "ts-loader": { - "version": "9.4.4", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.4.tgz", - "integrity": "sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", "dev": true, "requires": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", "micromatch": "^4.0.0", - "semver": "^7.3.4" + "semver": "^7.3.4", + "source-map": "^0.7.4" } }, "ts-node": { @@ -22151,15 +21784,15 @@ } }, "typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "devOptional": true }, "typesense": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/typesense/-/typesense-1.7.1.tgz", - "integrity": "sha512-RVtwprXDyU8MfAtLZ3PyH9WWRpEFGwij5BTu9I3VBjPIWtIvM/hbi2ogL/UK9dPXFECaxY8HlHrZz9djhJDZtg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/typesense/-/typesense-1.7.2.tgz", + "integrity": "sha512-hgQESOiyNJq+w2mpRJa/a1UMhWtJ/+sb0p7NoeCDSkikm9sasisJdnc7uhQchM6vTWKw2sMLWUBNbAhItR6zUQ==", "requires": { "axios": "^0.26.0", "loglevel": "^1.8.0" @@ -22176,9 +21809,9 @@ } }, "ua-parser-js": { - "version": "1.0.36", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.36.tgz", - "integrity": "sha512-znuyCIXzl8ciS3+y3fHJI/2OhQIXbXw9MWC/o3qwyR+RGppjZHrM27CGFSKCJXi2Kctiz537iOu2KnXs1lMQhw==" + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", + "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==" }, "uglify-js": { "version": "3.17.4", @@ -22199,6 +21832,11 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-1.0.0.tgz", "integrity": "sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==" }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -22216,15 +21854,6 @@ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true }, - "unzip-stream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.1.tgz", - "integrity": "sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==", - "requires": { - "binary": "^0.3.0", - "mkdirp": "^0.5.1" - } - }, "update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -22344,20 +21973,15 @@ "defaults": "^1.0.3" } }, - "web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" - }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "webpack": { - "version": "5.88.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", - "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", @@ -22499,7 +22123,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -22562,10 +22185,9 @@ "dev": true }, "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==" }, "yargs": { "version": "16.2.0", @@ -22602,7 +22224,8 @@ "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true }, "zip-stream": { "version": "5.0.1", diff --git a/server/package.json b/server/package.json index f073c738a..7c637215f 100644 --- a/server/package.json +++ b/server/package.json @@ -1,14 +1,10 @@ { "name": "immich", - "version": "1.84.0", + "version": "1.89.0", "description": "", "author": "", "private": true, "license": "UNLICENSED", - "bin": { - "immich": "./bin/cli.sh", - "immich-admin": "./bin/admin-cli.sh" - }, "scripts": { "build": "nest build", "format": "prettier --check .", @@ -26,7 +22,7 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "NODE_OPTIONS='--experimental-vm-modules --max_old_space_size=4096' jest --config test/e2e/jest-e2e.json --runInBand", + "test:e2e": "NODE_OPTIONS='--experimental-vm-modules' jest --config test/e2e/jest-e2e.json --runInBand", "typeorm": "typeorm", "typeorm:migrations:create": "typeorm migration:create", "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/infra/database.config.js", @@ -36,10 +32,12 @@ "typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run", "api:typescript": "bash ./bin/generate-open-api.sh web", "api:dart": "bash ./bin/generate-open-api.sh mobile", - "api:generate": "node ./bin/sync-spec-version.js && bash ./bin/generate-open-api.sh" + "api:generate": "node ./bin/sync-spec-version.js && bash ./bin/generate-open-api.sh", + "sql:generate": "node ./dist/infra/sql-generator/" }, "dependencies": { "@babel/runtime": "^7.22.11", + "@immich/cli": "^2.0.3", "@nestjs/bullmq": "^10.0.1", "@nestjs/common": "^10.2.2", "@nestjs/config": "^3.0.0", @@ -58,17 +56,15 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", - "exiftool-vendored": "~23.1.0", - "exiftool-vendored.pl": "12.67", + "exiftool-vendored": "~23.5.0", + "exiftool-vendored.pl": "12.70", "fluent-ffmpeg": "^2.1.2", "geo-tz": "^7.0.7", "glob": "^10.3.3", "handlebars": "^4.7.8", "i18n-iso-countries": "^7.6.0", - "immich": "^0.41.0", "ioredis": "^5.3.2", "joi": "^17.10.0", - "local-reverse-geocoder": "0.16.5", "lodash": "^4.17.21", "luxon": "^3.4.2", "mv": "^2.1.1", @@ -90,14 +86,14 @@ "@nestjs/testing": "^10.2.2", "@openapitools/openapi-generator-cli": "2.7.0", "@testcontainers/postgresql": "^10.2.1", - "@types/archiver": "^5.3.2", + "@types/archiver": "^6.0.0", "@types/bcrypt": "^5.0.0", "@types/cookie-parser": "^1.4.3", "@types/cron": "^2.0.1", "@types/express": "^4.17.17", "@types/fluent-ffmpeg": "^2.1.21", "@types/imagemin": "^8.0.1", - "@types/jest": "29.5.4", + "@types/jest": "29.5.10", "@types/jest-when": "^3.5.2", "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", @@ -120,6 +116,7 @@ "prettier-plugin-organize-imports": "^3.2.3", "rimraf": "^5.0.1", "source-map-support": "^0.5.21", + "sql-formatter": "^14.0.0", "supertest": "^6.3.3", "testcontainers": "^10.2.1", "ts-jest": "^29.1.1", diff --git a/server/resources/style-dark.json b/server/resources/style-dark.json new file mode 100644 index 000000000..9c4d39c6f --- /dev/null +++ b/server/resources/style-dark.json @@ -0,0 +1,1894 @@ +{ + "version": 8, + "name": "Immich Map", + "sources": { + "immich-map": { + "type": "vector", + "url": "https://api-l.cofractal.com/v0/maps/vt/overture" + } + }, + "sprite": "https://maputnik.github.io/osm-liberty/sprites/osm-liberty", + "glyphs": "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { "background-color": "rgb(42,42,41)" } + }, + { + "id": "park", + "type": "fill", + "source": "immich-map", + "source-layer": "park", + "paint": { + "fill-color": "rgba(8, 8, 7, 1)", + "fill-opacity": 0.7, + "fill-outline-color": "rgba(0, 0, 0, 1)", + "fill-antialias": false + } + }, + { + "id": "park_outline", + "type": "line", + "source": "immich-map", + "source-layer": "park", + "paint": { "line-dasharray": [1, 1.5], "line-color": "rgba(55, 55, 55, 1)" } + }, + { + "id": "landuse_residential", + "type": "fill", + "source": "immich-map", + "source-layer": "landuse", + "maxzoom": 8, + "filter": ["==", "class", "residential"], + "paint": { + "fill-color": { + "base": 1, + "stops": [ + [9, "rgba(59, 56, 56, 0.84)"], + [12, "hsla(35, 57%, 88%, 0.49)"] + ] + } + } + }, + { + "id": "landcover_wood", + "type": "fill", + "source": "immich-map", + "source-layer": "landcover", + "filter": ["all", ["==", "class", "wood"]], + "paint": { + "fill-antialias": false, + "fill-color": "rgba(186, 209, 173, 0.3)", + "fill-opacity": 0.4 + } + }, + { + "id": "landcover_grass", + "type": "fill", + "source": "immich-map", + "source-layer": "landcover", + "filter": ["all", ["==", "class", "grass"]], + "paint": { + "fill-antialias": false, + "fill-color": "rgba(176, 213, 154, 0.2)", + "fill-opacity": 0.3 + } + }, + { + "id": "landcover_ice", + "type": "fill", + "source": "immich-map", + "source-layer": "landcover", + "filter": ["all", ["==", "class", "ice"]], + "paint": { + "fill-antialias": false, + "fill-color": "rgba(94, 100, 100, 1)", + "fill-opacity": 0.8 + } + }, + { + "id": "landuse_cemetery", + "type": "fill", + "source": "immich-map", + "source-layer": "landuse", + "filter": ["==", "class", "cemetery"], + "layout": { "visibility": "none" }, + "paint": { "fill-color": "rgba(69, 69, 65, 1)" } + }, + { + "id": "landuse_hospital", + "type": "fill", + "source": "immich-map", + "source-layer": "landuse", + "filter": ["==", "class", "hospital"], + "layout": { "visibility": "none" }, + "paint": { "fill-color": "#fde" } + }, + { + "id": "landuse_school", + "type": "fill", + "source": "immich-map", + "source-layer": "landuse", + "filter": ["==", "class", "school"], + "layout": { "visibility": "none" }, + "paint": { "fill-color": "rgb(236,238,204)" } + }, + { + "id": "waterway_tunnel", + "type": "line", + "source": "immich-map", + "source-layer": "waterway", + "filter": ["all", ["==", "brunnel", "tunnel"]], + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [3, 3], + "line-gap-width": { + "stops": [ + [12, 0], + [20, 6] + ] + }, + "line-opacity": 1, + "line-width": { + "base": 1.4, + "stops": [ + [8, 1], + [20, 2] + ] + } + } + }, + { + "id": "waterway_river", + "type": "line", + "source": "immich-map", + "source-layer": "waterway", + "filter": ["all", ["==", "class", "river"], ["!=", "brunnel", "tunnel"]], + "layout": { "line-cap": "round" }, + "paint": { + "line-color": "rgba(78, 85, 88, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [11, 0.5], + [20, 6] + ] + } + } + }, + { + "id": "waterway_other", + "type": "line", + "source": "immich-map", + "source-layer": "waterway", + "filter": ["all", ["!=", "class", "river"], ["!=", "brunnel", "tunnel"]], + "layout": { "line-cap": "round" }, + "paint": { + "line-color": "#a0c8f0", + "line-width": { + "base": 1.3, + "stops": [ + [13, 0.5], + [20, 6] + ] + } + } + }, + { + "id": "water", + "type": "fill", + "source": "immich-map", + "source-layer": "water", + "filter": ["all", ["!=", "brunnel", "tunnel"]], + "paint": { "fill-color": "rgba(26, 26, 26, 1)" } + }, + { + "id": "landcover_sand", + "type": "fill", + "source": "immich-map", + "source-layer": "landcover", + "filter": ["all", ["==", "class", "sand"]], + "paint": { "fill-color": "rgba(193, 192, 188, 1)" } + }, + { + "id": "aeroway_fill", + "type": "fill", + "source": "immich-map", + "source-layer": "aeroway", + "minzoom": 11, + "filter": ["==", "$type", "Polygon"], + "paint": { "fill-color": "rgba(229, 228, 224, 1)", "fill-opacity": 0.7 } + }, + { + "id": "aeroway_runway", + "type": "line", + "source": "immich-map", + "source-layer": "aeroway", + "minzoom": 11, + "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "runway"]], + "paint": { + "line-color": "#f0ede9", + "line-width": { + "base": 1.2, + "stops": [ + [11, 3], + [20, 16] + ] + } + } + }, + { + "id": "aeroway_taxiway", + "type": "line", + "source": "immich-map", + "source-layer": "aeroway", + "minzoom": 11, + "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "taxiway"]], + "paint": { + "line-color": "#f0ede9", + "line-width": { + "base": 1.2, + "stops": [ + [11, 0.5], + [20, 6] + ] + } + } + }, + { + "id": "tunnel_motorway_link_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "rgba(52, 51, 49, 1)", + "line-dasharray": [0.5, 0.25], + "line-width": { + "base": 1.2, + "stops": [ + [12, 1], + [13, 3], + [14, 4], + [20, 15] + ] + } + } + }, + { + "id": "tunnel_service_track_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "service", "track"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#cfcdca", + "line-dasharray": [0.5, 0.25], + "line-width": { + "base": 1.2, + "stops": [ + [15, 1], + [16, 4], + [20, 11] + ] + } + } + }, + { + "id": "tunnel_link_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "rgba(40, 38, 36, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [12, 1], + [13, 3], + [14, 4], + [20, 15] + ] + } + } + }, + { + "id": "tunnel_street_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "street", "street_limited"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#cfcdca", + "line-opacity": { + "stops": [ + [12, 0], + [12.5, 1] + ] + }, + "line-width": { + "base": 1.2, + "stops": [ + [12, 0.5], + [13, 1], + [14, 4], + [20, 15] + ] + } + } + }, + { + "id": "tunnel_secondary_tertiary_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "secondary", "tertiary"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [8, 1.5], + [20, 17] + ] + } + } + }, + { + "id": "tunnel_trunk_primary_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "primary", "trunk"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "rgba(100, 86, 69, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0.4], + [6, 0.7], + [7, 1.75], + [20, 22] + ] + } + } + }, + { + "id": "tunnel_motorway_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "rgba(28, 26, 26, 1)", + "line-dasharray": [0.5, 0.25], + "line-width": { + "base": 1.2, + "stops": [ + [5, 0.4], + [6, 0.7], + [7, 1.75], + [20, 22] + ] + } + } + }, + { + "id": "tunnel_path_pedestrian", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "tunnel"], + ["in", "class", "path", "pedestrian"] + ], + "paint": { + "line-color": "hsl(0, 0%, 100%)", + "line-dasharray": [1, 0.75], + "line-width": { + "base": 1.2, + "stops": [ + [14, 0.5], + [20, 10] + ] + } + } + }, + { + "id": "tunnel_motorway_link", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fc8", + "line-width": { + "base": 1.2, + "stops": [ + [12.5, 0], + [13, 1.5], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "tunnel_service_track", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "service", "track"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fff", + "line-width": { + "base": 1.2, + "stops": [ + [15.5, 0], + [16, 2], + [20, 7.5] + ] + } + } + }, + { + "id": "tunnel_link", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "rgba(149, 139, 93, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [12.5, 0], + [13, 1.5], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "tunnel_minor", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "minor"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fff", + "line-width": { + "base": 1.2, + "stops": [ + [13.5, 0], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "tunnel_secondary_tertiary", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "secondary", "tertiary"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fff4c6", + "line-width": { + "base": 1.2, + "stops": [ + [6.5, 0], + [7, 0.5], + [20, 10] + ] + } + } + }, + { + "id": "tunnel_trunk_primary", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "primary", "trunk"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "rgba(116, 114, 97, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0], + [7, 1], + [20, 18] + ] + } + } + }, + { + "id": "tunnel_motorway", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "rgba(129, 124, 110, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0], + [7, 1], + [20, 18] + ] + } + } + }, + { + "id": "tunnel_major_rail", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "rail"]], + "paint": { + "line-color": "#bbb", + "line-width": { + "base": 1.4, + "stops": [ + [14, 0.4], + [15, 0.75], + [20, 2] + ] + } + } + }, + { + "id": "tunnel_major_rail_hatching", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "rail"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": { + "base": 1.4, + "stops": [ + [14.5, 0], + [15, 3], + [20, 8] + ] + } + } + }, + { + "id": "tunnel_transit_rail", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "transit"]], + "paint": { + "line-color": "#bbb", + "line-width": { + "base": 1.4, + "stops": [ + [14, 0.4], + [15, 0.75], + [20, 2] + ] + } + } + }, + { + "id": "tunnel_transit_rail_hatching", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "transit"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": { + "base": 1.4, + "stops": [ + [14.5, 0], + [15, 3], + [20, 8] + ] + } + } + }, + { + "id": "road_motorway_link_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 12, + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["==", "ramp", 1]], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(65, 63, 62, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [12, 1], + [13, 3], + [14, 4], + [20, 15] + ] + } + } + }, + { + "id": "road_minor_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "minor"], + ["!=", "ramp", 1] + ], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "rgba(17, 17, 17, 1)", + "line-opacity": { + "stops": [ + [12, 0], + [12.5, 1] + ] + }, + "line-width": { + "base": 1.2, + "stops": [ + [12, 0.5], + [13, 1], + [14, 4], + [20, 20] + ] + } + } + }, + { + "id": "road_secondary_tertiary_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "secondary", "tertiary"], + ["!=", "ramp", 1] + ], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "rgba(102, 102, 102, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [8, 1.5], + [20, 17] + ] + } + } + }, + { + "id": "road_trunk_primary_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "primary", "trunk"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "rgba(61, 61, 61, 0.6)", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0.4], + [6, 0.7], + [7, 1.75], + [20, 22] + ] + } + } + }, + { + "id": "road_motorway_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 5, + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["!=", "ramp", 1]], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "rgba(61, 61, 61, 0.6)", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0.4], + [6, 0.7], + [7, 1.75], + [20, 22] + ] + } + } + }, + { + "id": "road_motorway_link", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 12, + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["==", "ramp", 1]], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "rgba(184, 184, 179, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [12.5, 0], + [13, 1.5], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "road_service_track", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "service", "track"]], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "rgba(84, 81, 81, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [15.5, 0], + [16, 2], + [20, 7.5] + ] + } + } + }, + { + "id": "road_link", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["==", "ramp", 1], + ["!in", "class", "pedestrian", "path", "track", "service", "motorway"] + ], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "#fea", + "line-width": { + "base": 1.2, + "stops": [ + [12.5, 0], + [13, 1.5], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "road_minor", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "minor"] + ], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "rgba(40, 40, 40, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [13.5, 0], + [14, 2.5], + [20, 18] + ] + } + } + }, + { + "id": "road_secondary_tertiary", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "secondary", "tertiary"]], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "rgba(36, 33, 33, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [6.5, 0], + [8, 0.5], + [20, 13] + ] + } + } + }, + { + "id": "road_trunk_primary", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "primary", "trunk"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "rgba(61, 61, 61, 0.6)", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0], + [7, 1], + [20, 18] + ] + } + } + }, + { + "id": "road_motorway", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 5, + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["!=", "ramp", 1]], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "rgba(61, 61, 61, 0.6)", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0], + [7, 1], + [20, 18] + ] + } + } + }, + { + "id": "road_major_rail", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "rail"]], + "paint": { + "line-color": "#bbb", + "line-width": { + "base": 1.4, + "stops": [ + [14, 0.4], + [15, 0.75], + [20, 2] + ] + } + } + }, + { + "id": "road_major_rail_hatching", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "rail"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": { + "base": 1.4, + "stops": [ + [14.5, 0], + [15, 3], + [20, 8] + ] + } + } + }, + { + "id": "road_transit_rail", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "transit"]], + "paint": { + "line-color": "#bbb", + "line-width": { + "base": 1.4, + "stops": [ + [14, 0.4], + [15, 0.75], + [20, 2] + ] + } + } + }, + { + "id": "road_transit_rail_hatching", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "transit"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": { + "base": 1.4, + "stops": [ + [14.5, 0], + [15, 3], + [20, 8] + ] + } + } + }, + { + "id": "bridge_motorway_link_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "bridge"]], + "layout": { "line-join": "round", "visibility": "visible" }, + "paint": { + "line-color": "rgba(75, 68, 63, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [12, 1], + [13, 3], + [14, 4], + [20, 15] + ] + } + } + }, + { + "id": "bridge_service_track_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "service", "track"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#cfcdca", + "line-width": { + "base": 1.2, + "stops": [ + [15, 1], + [16, 4], + [20, 11] + ] + } + } + }, + { + "id": "bridge_link_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "link"], ["==", "brunnel", "bridge"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [12, 1], + [13, 3], + [14, 4], + [20, 15] + ] + } + } + }, + { + "id": "bridge_street_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "street", "street_limited"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "hsl(36, 6%, 74%)", + "line-opacity": { + "stops": [ + [12, 0], + [12.5, 1] + ] + }, + "line-width": { + "base": 1.2, + "stops": [ + [12, 0.5], + [13, 1], + [14, 4], + [20, 25] + ] + } + } + }, + { + "id": "bridge_path_pedestrian_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "bridge"], + ["in", "class", "path", "pedestrian"] + ], + "paint": { + "line-color": "hsl(35, 6%, 80%)", + "line-dasharray": [1, 0], + "line-width": { + "base": 1.2, + "stops": [ + [14, 1.5], + [20, 18] + ] + } + } + }, + { + "id": "bridge_secondary_tertiary_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "secondary", "tertiary"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "rgba(61, 57, 52, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [8, 1.5], + [20, 17] + ] + } + } + }, + { + "id": "bridge_trunk_primary_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "primary", "trunk"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "rgba(102, 102, 102, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0.4], + [6, 0.7], + [7, 1.75], + [20, 22] + ] + } + } + }, + { + "id": "bridge_motorway_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], + "layout": { "line-join": "round", "visibility": "none" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0.4], + [6, 0.7], + [7, 1.75], + [20, 22] + ] + } + } + }, + { + "id": "bridge_path_pedestrian", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "bridge"], + ["in", "class", "path", "pedestrian"] + ], + "paint": { + "line-color": "hsl(0, 0%, 100%)", + "line-dasharray": [1, 0.3], + "line-width": { + "base": 1.2, + "stops": [ + [14, 0.5], + [20, 10] + ] + } + } + }, + { + "id": "bridge_motorway_link", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "bridge"]], + "layout": { "line-join": "round", "visibility": "none" }, + "paint": { + "line-color": "#fc8", + "line-width": { + "base": 1.2, + "stops": [ + [12.5, 0], + [13, 1.5], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "bridge_service_track", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "service", "track"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fff", + "line-width": { + "base": 1.2, + "stops": [ + [15.5, 0], + [16, 2], + [20, 7.5] + ] + } + } + }, + { + "id": "bridge_link", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "link"], ["==", "brunnel", "bridge"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fea", + "line-width": { + "base": 1.2, + "stops": [ + [12.5, 0], + [13, 1.5], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "bridge_street", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "minor"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fff", + "line-width": { + "base": 1.2, + "stops": [ + [13.5, 0], + [14, 2.5], + [20, 18] + ] + } + } + }, + { + "id": "bridge_secondary_tertiary", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "secondary", "tertiary"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "rgba(73, 71, 68, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [6.5, 0], + [7, 0.5], + [20, 10] + ] + } + } + }, + { + "id": "bridge_trunk_primary", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "primary", "trunk"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "rgba(147, 147, 143, 1)", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0], + [7, 1], + [20, 18] + ] + } + } + }, + { + "id": "bridge_motorway", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], + "layout": { "line-join": "round", "visibility": "none" }, + "paint": { + "line-color": "#fc8", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0], + [7, 1], + [20, 18] + ] + } + } + }, + { + "id": "bridge_major_rail", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "bridge"]], + "paint": { + "line-color": "#bbb", + "line-width": { + "base": 1.4, + "stops": [ + [14, 0.4], + [15, 0.75], + [20, 2] + ] + } + } + }, + { + "id": "bridge_major_rail_hatching", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "bridge"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": { + "base": 1.4, + "stops": [ + [14.5, 0], + [15, 3], + [20, 8] + ] + } + } + }, + { + "id": "bridge_transit_rail", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "transit"], ["==", "brunnel", "bridge"]], + "paint": { + "line-color": "#bbb", + "line-width": { + "base": 1.4, + "stops": [ + [14, 0.4], + [15, 0.75], + [20, 2] + ] + } + } + }, + { + "id": "bridge_transit_rail_hatching", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "transit"], ["==", "brunnel", "bridge"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": { + "base": 1.4, + "stops": [ + [14.5, 0], + [15, 3], + [20, 8] + ] + } + } + }, + { + "id": "building", + "type": "fill", + "source": "immich-map", + "source-layer": "building", + "minzoom": 13, + "maxzoom": 14, + "paint": { + "fill-color": "rgba(20, 20, 20, 1)", + "fill-outline-color": { + "base": 1, + "stops": [ + [13, "rgba(10, 10, 9, 0.32)"], + [14, "rgba(22, 22, 22, 1)"] + ] + } + } + }, + { + "id": "building-3d", + "type": "fill-extrusion", + "source": "immich-map", + "source-layer": "building", + "minzoom": 14, + "paint": { + "fill-extrusion-color": "rgba(57, 57, 57, 1)", + "fill-extrusion-height": { + "property": "render_height", + "type": "identity" + }, + "fill-extrusion-base": { + "property": "render_min_height", + "type": "identity" + }, + "fill-extrusion-opacity": 0.8 + } + }, + { + "id": "boundary_state", + "type": "line", + "source": "immich-map", + "source-layer": "boundary" + }, + { + "id": "boundary_3", + "type": "line", + "source": "immich-map", + "source-layer": "boundary", + "minzoom": 8, + "filter": ["all", ["in", "admin_level", 3, 4]], + "layout": { "line-join": "round", "visibility": "visible" }, + "paint": { + "line-color": "#9e9cab", + "line-dasharray": [5, 1], + "line-width": { + "base": 1, + "stops": [ + [4, 0.4], + [5, 1], + [12, 1.8] + ] + } + } + }, + { + "id": "boundary_country", + "type": "line", + "source": "immich-map", + "source-layer": "boundary", + "maxzoom": 5, + "filter": ["all", ["==", "admin_level", 2], ["!has", "claimed_by"]], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-color": "hsl(248, 1%, 41%)", + "line-opacity": { + "base": 1, + "stops": [ + [0, 0.4], + [4, 1] + ] + }, + "line-width": { + "base": 1, + "stops": [ + [3, 1], + [5, 1.2], + [12, 3] + ] + } + } + }, + { + "id": "boundary_2_z5-", + "type": "line", + "source": "immich-map", + "source-layer": "boundary", + "minzoom": 5, + "filter": ["all", ["==", "admin_level", 2]], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "none" + }, + "paint": { + "line-color": "hsl(248, 1%, 41%)", + "line-opacity": { + "base": 1, + "stops": [ + [0, 0.4], + [4, 1] + ] + }, + "line-width": { + "base": 1, + "stops": [ + [3, 1], + [5, 1.2], + [12, 3] + ] + } + } + }, + { + "id": "water_name_line", + "type": "symbol", + "source": "immich-map", + "source-layer": "waterway", + "filter": ["all", ["==", "$type", "LineString"]], + "layout": { + "text-field": "{name}", + "text-font": ["Open Sans Bold"], + "text-max-width": 5, + "text-size": 12, + "symbol-placement": "line" + }, + "paint": { + "text-color": "rgba(70, 178, 228, 1)", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 0 + } + }, + { + "id": "water_name_point", + "type": "symbol", + "source": "immich-map", + "source-layer": "water_name", + "filter": ["==", "$type", "Point"], + "layout": { + "text-field": "{name}", + "text-font": ["Open Sans Regular"], + "text-max-width": 5, + "text-size": 12 + }, + "paint": { + "text-color": "rgba(193, 193, 193, 1)", + "text-halo-color": "rgba(92, 105, 106, 0.7)", + "text-halo-width": 1 + } + }, + { + "id": "poi_z16", + "type": "symbol", + "source": "immich-map", + "source-layer": "poi", + "minzoom": 16, + "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 20]], + "layout": { + "icon-image": "{class}_11", + "text-anchor": "top", + "text-field": "{name}", + "text-font": ["Open Sans Italic"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-size": 12, + "visibility": "none" + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "poi_z15", + "type": "symbol", + "source": "immich-map", + "source-layer": "poi", + "minzoom": 15, + "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 7], ["<", "rank", 20]], + "layout": { + "icon-image": "{class}_11", + "text-anchor": "top", + "text-field": "{name}", + "text-font": ["Open Sans Italic"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-size": 12 + }, + "paint": { + "text-color": "rgba(252, 135, 145, 1)", + "text-halo-blur": 0.5, + "text-halo-color": "rgba(54, 49, 49, 1)", + "text-halo-width": 1 + } + }, + { + "id": "poi_z14", + "type": "symbol", + "source": "immich-map", + "source-layer": "poi", + "minzoom": 14, + "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 1], ["<", "rank", 7]], + "layout": { + "icon-image": "{class}_11", + "text-anchor": "top", + "text-field": "{name}", + "text-font": ["Open Sans Bold Italic"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-size": 12 + }, + "paint": { + "text-color": "rgba(153, 242, 197, 1)", + "text-halo-blur": 0.5, + "text-halo-color": "rgba(0, 0, 0, 1)", + "text-halo-width": 0 + } + }, + { + "id": "poi_transit", + "type": "symbol", + "source": "immich-map", + "source-layer": "poi", + "filter": ["all", ["in", "class", "bus", "rail", "airport"]], + "layout": { + "icon-image": "{class}_11", + "text-anchor": "left", + "text-field": "{name_en}", + "text-font": ["Open Sans Italic"], + "text-max-width": 9, + "text-offset": [0.9, 0], + "text-size": 12, + "visibility": "none" + }, + "paint": { + "text-color": "#4898ff", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "road_label", + "type": "symbol", + "source": "immich-map", + "source-layer": "transportation_name", + "filter": ["all"], + "layout": { + "symbol-placement": "line", + "text-anchor": "center", + "text-field": "{name}", + "text-font": ["Open Sans Bold"], + "text-offset": [0, 0.15], + "text-size": { + "base": 1, + "stops": [ + [13, 12], + [14, 13] + ] + } + }, + "paint": { + "text-color": "rgba(210, 210, 210, 1)", + "text-halo-blur": 0.5, + "text-halo-width": 1 + } + }, + { + "id": "road_shield", + "type": "symbol", + "source": "immich-map", + "source-layer": "transportation_name", + "minzoom": 7, + "filter": ["all", ["<=", "ref_length", 6]], + "layout": { + "icon-image": "default_{ref_length}", + "icon-rotation-alignment": "viewport", + "symbol-placement": { + "base": 1, + "stops": [ + [10, "point"], + [11, "line"] + ] + }, + "symbol-spacing": 500, + "text-field": "{ref}", + "text-font": ["Open Sans Regular"], + "text-offset": [0, 0.1], + "text-rotation-alignment": "viewport", + "text-size": 10, + "icon-size": 0.8, + "visibility": "none" + } + }, + { + "id": "place_other", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "filter": ["all", ["in", "class", "hamlet", "island", "islet", "neighbourhood", "suburb", "quarter"]], + "layout": { + "text-field": "{name_en}", + "text-font": ["Open Sans Italic"], + "text-letter-spacing": 0.1, + "text-max-width": 9, + "text-size": { + "base": 1.2, + "stops": [ + [12, 10], + [15, 14] + ] + }, + "text-transform": "uppercase" + }, + "paint": { + "text-color": "rgba(255, 255, 255, 1)", + "text-halo-color": "rgba(0, 0, 0, 0.8)", + "text-halo-width": 1.2 + } + }, + { + "id": "place_village", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "filter": ["all", ["==", "class", "village"]], + "layout": { + "text-field": "{name_en}", + "text-font": ["Open Sans Regular"], + "text-max-width": 8, + "text-size": { + "base": 1.2, + "stops": [ + [10, 12], + [15, 22] + ] + } + }, + "paint": { + "text-color": "rgba(189, 189, 189, 1)", + "text-halo-color": "rgba(0, 0, 0, 0.8)", + "text-halo-width": 1 + } + }, + { + "id": "place_town", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "filter": ["all", ["==", "class", "town"]], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [0, "dot_9"], + [8, ""] + ] + }, + "text-anchor": "bottom", + "text-field": "{name_en}", + "text-font": ["Klokantech Noto Sans Regular"], + "text-max-width": 8, + "text-offset": [0, 0], + "text-size": { + "base": 1.2, + "stops": [ + [7, 12], + [11, 16] + ] + } + }, + "paint": { + "text-color": "rgba(247, 247, 247, 0.5)", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 0 + } + }, + { + "id": "place_city", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "minzoom": 5, + "filter": ["all", ["==", "class", "city"]], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [0, "dot_9"], + [8, ""] + ] + }, + "text-anchor": "bottom", + "text-field": "{name_en}", + "text-font": ["Open Sans Semibold"], + "text-max-width": 8, + "text-offset": [0, 0], + "text-size": { + "base": 0.5, + "stops": [ + [7, 14], + [11, 24] + ] + }, + "icon-allow-overlap": true, + "icon-optional": false + }, + "paint": { + "text-color": "rgba(230, 230, 230, 1)", + "text-halo-color": "rgba(0, 0, 0, 0.8)", + "text-halo-width": 0.5 + } + }, + { + "id": "state", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "minzoom": 4, + "maxzoom": 6, + "filter": ["all", ["==", "class", "state"]], + "layout": { + "text-field": "{name_en}", + "text-font": ["Klokantech Noto Sans Regular"], + "text-size": { + "stops": [ + [4, 9], + [6, 15] + ] + }, + "text-transform": "uppercase" + }, + "paint": { + "text-color": "rgba(226, 219, 219, 1)", + "text-halo-color": "rgba(0, 0, 0, 0.7)", + "text-halo-width": 1, + "text-halo-blur": 0, + "text-translate": [1, 1] + } + }, + { + "id": "country_3", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "filter": ["all", [">=", "rank", 3], ["==", "class", "country"]], + "layout": { + "text-field": "{name_en}", + "text-font": ["Klokantech Noto Sans Bold"], + "text-max-width": 6.25, + "text-size": { + "stops": [ + [1, 11], + [4, 17] + ] + }, + "text-transform": "none" + }, + "paint": { + "text-color": "rgba(226, 221, 221, 1)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0, 0, 0, 0.8)", + "text-halo-width": 1 + } + }, + { + "id": "country_2", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "filter": ["all", ["==", "rank", 2], ["==", "class", "country"]], + "layout": { + "text-field": "{name_en}", + "text-font": ["Klokantech Noto Sans Bold"], + "text-max-width": 6.25, + "text-size": { + "stops": [ + [1, 11], + [4, 17] + ] + }, + "text-transform": "none" + }, + "paint": { + "text-color": "rgba(226, 221, 221, 1)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0, 0, 0, 0.8)", + "text-halo-width": 1 + } + }, + { + "id": "country_1", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "filter": ["all", ["==", "rank", 1], ["==", "class", "country"]], + "layout": { + "text-field": "{name_en}", + "text-font": ["Klokantech Noto Sans Bold"], + "text-max-width": 6.25, + "text-size": { + "stops": [ + [1, 11], + [4, 17] + ] + }, + "text-transform": "none" + }, + "paint": { + "text-color": "rgba(226, 221, 221, 1)", + "text-halo-blur": 1, + "text-halo-color": "rgba(0, 0, 0, 0.8)", + "text-halo-width": 1 + } + } + ], + "id": "immich-map-dark" +} diff --git a/server/resources/style-light.json b/server/resources/style-light.json new file mode 100644 index 000000000..7d4124f22 --- /dev/null +++ b/server/resources/style-light.json @@ -0,0 +1,1999 @@ +{ + "version": 8, + "name": "Immich Map", + "sources": { + "immich-map": { + "type": "vector", + "url": "https://api-l.cofractal.com/v0/maps/vt/overture" + } + }, + "sprite": "https://maputnik.github.io/osm-liberty/sprites/osm-liberty", + "glyphs": "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { "background-color": "rgba(232, 244, 237, 1)" } + }, + { + "id": "park", + "type": "fill", + "source": "immich-map", + "source-layer": "park", + "paint": { + "fill-color": "#d8e8c8", + "fill-opacity": 0.7, + "fill-outline-color": "rgba(95, 208, 100, 1)" + } + }, + { + "id": "park_outline", + "type": "line", + "source": "immich-map", + "source-layer": "park", + "paint": { + "line-dasharray": [1, 1.5], + "line-color": "rgba(228, 241, 215, 1)" + } + }, + { + "id": "landuse_residential", + "type": "fill", + "source": "immich-map", + "source-layer": "landuse", + "maxzoom": 8, + "filter": ["==", "class", "residential"], + "paint": { + "fill-color": { + "base": 1, + "stops": [ + [9, "hsla(0, 3%, 85%, 0.84)"], + [12, "hsla(35, 57%, 88%, 0.49)"] + ] + } + } + }, + { + "id": "landcover_wood", + "type": "fill", + "source": "immich-map", + "source-layer": "landcover", + "filter": ["all", ["==", "class", "wood"]], + "paint": { + "fill-antialias": false, + "fill-color": "hsla(98, 61%, 72%, 0.7)", + "fill-opacity": 0.4 + } + }, + { + "id": "landcover_grass", + "type": "fill", + "source": "immich-map", + "source-layer": "landcover", + "filter": ["all", ["==", "class", "grass"]], + "paint": { + "fill-antialias": false, + "fill-color": "rgba(176, 213, 154, 1)", + "fill-opacity": 0.3 + } + }, + { + "id": "landcover_ice", + "type": "fill", + "source": "immich-map", + "source-layer": "landcover", + "filter": ["all", ["==", "class", "ice"]], + "paint": { + "fill-antialias": false, + "fill-color": "rgba(224, 236, 236, 1)", + "fill-opacity": 0.8 + } + }, + { + "id": "landuse_cemetery", + "type": "fill", + "source": "immich-map", + "source-layer": "landuse", + "filter": ["==", "class", "cemetery"], + "paint": { "fill-color": "hsl(75, 37%, 81%)" } + }, + { + "id": "landuse_hospital", + "type": "fill", + "source": "immich-map", + "source-layer": "landuse", + "filter": ["==", "class", "hospital"], + "paint": { "fill-color": "#fde" } + }, + { + "id": "landuse_school", + "type": "fill", + "source": "immich-map", + "source-layer": "landuse", + "filter": ["==", "class", "school"], + "paint": { "fill-color": "rgb(236,238,204)" } + }, + { + "id": "waterway_tunnel", + "type": "line", + "source": "immich-map", + "source-layer": "waterway", + "filter": ["all", ["==", "brunnel", "tunnel"]], + "paint": { + "line-color": "#a0c8f0", + "line-dasharray": [3, 3], + "line-gap-width": { + "stops": [ + [12, 0], + [20, 6] + ] + }, + "line-opacity": 1, + "line-width": { + "base": 1.4, + "stops": [ + [8, 1], + [20, 2] + ] + } + } + }, + { + "id": "waterway_river", + "type": "line", + "source": "immich-map", + "source-layer": "waterway", + "filter": ["all", ["==", "class", "river"], ["!=", "brunnel", "tunnel"]], + "layout": { "line-cap": "round" }, + "paint": { + "line-color": "#a0c8f0", + "line-width": { + "base": 1.2, + "stops": [ + [11, 0.5], + [20, 6] + ] + } + } + }, + { + "id": "waterway_other", + "type": "line", + "source": "immich-map", + "source-layer": "waterway", + "filter": ["all", ["!=", "class", "river"], ["!=", "brunnel", "tunnel"]], + "layout": { "line-cap": "round" }, + "paint": { + "line-color": "#a0c8f0", + "line-width": { + "base": 1.3, + "stops": [ + [13, 0.5], + [20, 6] + ] + } + } + }, + { + "id": "water", + "type": "fill", + "source": "immich-map", + "source-layer": "water", + "filter": ["all", ["!=", "brunnel", "tunnel"]], + "paint": { "fill-color": "rgba(148, 209, 236, 0.66)" } + }, + { + "id": "landcover_sand", + "type": "fill", + "source": "immich-map", + "source-layer": "landcover", + "filter": ["all", ["==", "class", "sand"]], + "paint": { "fill-color": "rgba(247, 239, 195, 1)" } + }, + { + "id": "aeroway_fill", + "type": "fill", + "source": "immich-map", + "source-layer": "aeroway", + "minzoom": 11, + "filter": ["==", "$type", "Polygon"], + "paint": { "fill-color": "rgba(229, 228, 224, 1)", "fill-opacity": 0.7 } + }, + { + "id": "aeroway_runway", + "type": "line", + "source": "immich-map", + "source-layer": "aeroway", + "minzoom": 11, + "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "runway"]], + "paint": { + "line-color": "#f0ede9", + "line-width": { + "base": 1.2, + "stops": [ + [11, 3], + [20, 16] + ] + } + } + }, + { + "id": "aeroway_taxiway", + "type": "line", + "source": "immich-map", + "source-layer": "aeroway", + "minzoom": 11, + "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "taxiway"]], + "paint": { + "line-color": "#f0ede9", + "line-width": { + "base": 1.2, + "stops": [ + [11, 0.5], + [20, 6] + ] + } + } + }, + { + "id": "tunnel_motorway_link_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-dasharray": [0.5, 0.25], + "line-width": { + "base": 1.2, + "stops": [ + [12, 1], + [13, 3], + [14, 4], + [20, 15] + ] + } + } + }, + { + "id": "tunnel_service_track_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "service", "track"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#cfcdca", + "line-dasharray": [0.5, 0.25], + "line-width": { + "base": 1.2, + "stops": [ + [15, 1], + [16, 4], + [20, 11] + ] + } + } + }, + { + "id": "tunnel_link_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [12, 1], + [13, 3], + [14, 4], + [20, 15] + ] + } + } + }, + { + "id": "tunnel_street_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "street", "street_limited"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#cfcdca", + "line-opacity": { + "stops": [ + [12, 0], + [12.5, 1] + ] + }, + "line-width": { + "base": 1.2, + "stops": [ + [12, 0.5], + [13, 1], + [14, 4], + [20, 15] + ] + } + } + }, + { + "id": "tunnel_secondary_tertiary_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "secondary", "tertiary"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [8, 1.5], + [20, 17] + ] + } + } + }, + { + "id": "tunnel_trunk_primary_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "primary", "trunk"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0.4], + [6, 0.7], + [7, 1.75], + [20, 22] + ] + } + } + }, + { + "id": "tunnel_motorway_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-dasharray": [0.5, 0.25], + "line-width": { + "base": 1.2, + "stops": [ + [5, 0.4], + [6, 0.7], + [7, 1.75], + [20, 22] + ] + } + } + }, + { + "id": "tunnel_path_pedestrian", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "tunnel"], + ["in", "class", "path", "pedestrian"] + ], + "paint": { + "line-color": "hsl(0, 0%, 100%)", + "line-dasharray": [1, 0.75], + "line-width": { + "base": 1.2, + "stops": [ + [14, 0.5], + [20, 10] + ] + } + } + }, + { + "id": "tunnel_motorway_link", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fc8", + "line-width": { + "base": 1.2, + "stops": [ + [12.5, 0], + [13, 1.5], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "tunnel_service_track", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "service", "track"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fff", + "line-width": { + "base": 1.2, + "stops": [ + [15.5, 0], + [16, 2], + [20, 7.5] + ] + } + } + }, + { + "id": "tunnel_link", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fff4c6", + "line-width": { + "base": 1.2, + "stops": [ + [12.5, 0], + [13, 1.5], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "tunnel_minor", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "minor"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fff", + "line-width": { + "base": 1.2, + "stops": [ + [13.5, 0], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "tunnel_secondary_tertiary", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "secondary", "tertiary"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fff4c6", + "line-width": { + "base": 1.2, + "stops": [ + [6.5, 0], + [7, 0.5], + [20, 10] + ] + } + } + }, + { + "id": "tunnel_trunk_primary", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "primary", "trunk"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fff4c6", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0], + [7, 1], + [20, 18] + ] + } + } + }, + { + "id": "tunnel_motorway", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#ffdaa6", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0], + [7, 1], + [20, 18] + ] + } + } + }, + { + "id": "tunnel_major_rail", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "rail"]], + "paint": { + "line-color": "#bbb", + "line-width": { + "base": 1.4, + "stops": [ + [14, 0.4], + [15, 0.75], + [20, 2] + ] + } + } + }, + { + "id": "tunnel_major_rail_hatching", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "rail"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": { + "base": 1.4, + "stops": [ + [14.5, 0], + [15, 3], + [20, 8] + ] + } + } + }, + { + "id": "tunnel_transit_rail", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "transit"]], + "paint": { + "line-color": "#bbb", + "line-width": { + "base": 1.4, + "stops": [ + [14, 0.4], + [15, 0.75], + [20, 2] + ] + } + } + }, + { + "id": "tunnel_transit_rail_hatching", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "transit"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": { + "base": 1.4, + "stops": [ + [14.5, 0], + [15, 3], + [20, 8] + ] + } + } + }, + { + "id": "road_area_pattern", + "type": "fill", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "$type", "Polygon"]], + "paint": { "fill-pattern": "pedestrian_polygon" } + }, + { + "id": "road_motorway_link_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 12, + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["==", "ramp", 1]], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [12, 1], + [13, 3], + [14, 4], + [20, 15] + ] + } + } + }, + { + "id": "road_service_track_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "service", "track"]], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "#cfcdca", + "line-width": { + "base": 1.2, + "stops": [ + [15, 1], + [16, 4], + [20, 11] + ] + } + } + }, + { + "id": "road_link_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["!in", "class", "pedestrian", "path", "track", "service", "motorway"], + ["==", "ramp", 1] + ], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [12, 1], + [13, 3], + [14, 4], + [20, 15] + ] + } + } + }, + { + "id": "road_minor_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "minor"], + ["!=", "ramp", 1] + ], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "#cfcdca", + "line-opacity": { + "stops": [ + [12, 0], + [12.5, 1] + ] + }, + "line-width": { + "base": 1.2, + "stops": [ + [12, 0.5], + [13, 1], + [14, 4], + [20, 20] + ] + } + } + }, + { + "id": "road_secondary_tertiary_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "secondary", "tertiary"], + ["!=", "ramp", 1] + ], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [8, 1.5], + [20, 17] + ] + } + } + }, + { + "id": "road_trunk_primary_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "primary", "trunk"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0.4], + [6, 0.7], + [7, 1.75], + [20, 22] + ] + } + } + }, + { + "id": "road_motorway_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 5, + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["!=", "ramp", 1]], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0.4], + [6, 0.7], + [7, 1.75], + [20, 22] + ] + } + } + }, + { + "id": "road_path_pedestrian", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 14, + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "path", "pedestrian"] + ], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "hsl(0, 0%, 100%)", + "line-dasharray": [1, 0.7], + "line-width": { + "base": 1.2, + "stops": [ + [14, 1], + [20, 10] + ] + } + } + }, + { + "id": "road_motorway_link", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 12, + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["==", "ramp", 1]], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "#fc8", + "line-width": { + "base": 1.2, + "stops": [ + [12.5, 0], + [13, 1.5], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "road_service_track", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "service", "track"]], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "#fff", + "line-width": { + "base": 1.2, + "stops": [ + [15.5, 0], + [16, 2], + [20, 7.5] + ] + } + } + }, + { + "id": "road_link", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 13, + "filter": [ + "all", + ["!in", "brunnel", "bridge", "tunnel"], + ["==", "ramp", 1], + ["!in", "class", "pedestrian", "path", "track", "service", "motorway"] + ], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "#fea", + "line-width": { + "base": 1.2, + "stops": [ + [12.5, 0], + [13, 1.5], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "road_minor", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["!in", "brunnel", "bridge", "tunnel"], + ["in", "class", "minor"] + ], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "#fff", + "line-width": { + "base": 1.2, + "stops": [ + [13.5, 0], + [14, 2.5], + [20, 18] + ] + } + } + }, + { + "id": "road_secondary_tertiary", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "secondary", "tertiary"]], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "#fea", + "line-width": { + "base": 1.2, + "stops": [ + [6.5, 0], + [8, 0.5], + [20, 13] + ] + } + } + }, + { + "id": "road_trunk_primary", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "primary", "trunk"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fea", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0], + [7, 1], + [20, 18] + ] + } + } + }, + { + "id": "road_motorway", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 5, + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["!=", "ramp", 1]], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": { + "base": 1, + "stops": [ + [5, "hsl(26, 87%, 62%)"], + [6, "#fc8"] + ] + }, + "line-width": { + "base": 1.2, + "stops": [ + [5, 0], + [7, 1], + [20, 18] + ] + } + } + }, + { + "id": "road_major_rail", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "rail"]], + "paint": { + "line-color": "#bbb", + "line-width": { + "base": 1.4, + "stops": [ + [14, 0.4], + [15, 0.75], + [20, 2] + ] + } + } + }, + { + "id": "road_major_rail_hatching", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "rail"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": { + "base": 1.4, + "stops": [ + [14.5, 0], + [15, 3], + [20, 8] + ] + } + } + }, + { + "id": "road_transit_rail", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "transit"]], + "paint": { + "line-color": "#bbb", + "line-width": { + "base": 1.4, + "stops": [ + [14, 0.4], + [15, 0.75], + [20, 2] + ] + } + } + }, + { + "id": "road_transit_rail_hatching", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "transit"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": { + "base": 1.4, + "stops": [ + [14.5, 0], + [15, 3], + [20, 8] + ] + } + } + }, + { + "id": "road_one_way_arrow", + "type": "symbol", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 15, + "filter": ["==", "oneway", 1], + "layout": { "icon-image": "arrow", "symbol-placement": "line" } + }, + { + "id": "road_one_way_arrow_opposite", + "type": "symbol", + "source": "immich-map", + "source-layer": "transportation", + "minzoom": 15, + "filter": ["==", "oneway", -1], + "layout": { + "icon-image": "arrow", + "symbol-placement": "line", + "icon-rotate": 180 + } + }, + { + "id": "bridge_motorway_link_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "bridge"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [12, 1], + [13, 3], + [14, 4], + [20, 15] + ] + } + } + }, + { + "id": "bridge_service_track_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "service", "track"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#cfcdca", + "line-width": { + "base": 1.2, + "stops": [ + [15, 1], + [16, 4], + [20, 11] + ] + } + } + }, + { + "id": "bridge_link_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "link"], ["==", "brunnel", "bridge"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [12, 1], + [13, 3], + [14, 4], + [20, 15] + ] + } + } + }, + { + "id": "bridge_street_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "street", "street_limited"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "hsl(36, 6%, 74%)", + "line-opacity": { + "stops": [ + [12, 0], + [12.5, 1] + ] + }, + "line-width": { + "base": 1.2, + "stops": [ + [12, 0.5], + [13, 1], + [14, 4], + [20, 25] + ] + } + } + }, + { + "id": "bridge_path_pedestrian_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "bridge"], + ["in", "class", "path", "pedestrian"] + ], + "paint": { + "line-color": "hsl(35, 6%, 80%)", + "line-dasharray": [1, 0], + "line-width": { + "base": 1.2, + "stops": [ + [14, 1.5], + [20, 18] + ] + } + } + }, + { + "id": "bridge_secondary_tertiary_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "secondary", "tertiary"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [8, 1.5], + [20, 17] + ] + } + } + }, + { + "id": "bridge_trunk_primary_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "primary", "trunk"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0.4], + [6, 0.7], + [7, 1.75], + [20, 22] + ] + } + } + }, + { + "id": "bridge_motorway_casing", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#e9ac77", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0.4], + [6, 0.7], + [7, 1.75], + [20, 22] + ] + } + } + }, + { + "id": "bridge_path_pedestrian", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": [ + "all", + ["==", "$type", "LineString"], + ["==", "brunnel", "bridge"], + ["in", "class", "path", "pedestrian"] + ], + "paint": { + "line-color": "hsl(0, 0%, 100%)", + "line-dasharray": [1, 0.3], + "line-width": { + "base": 1.2, + "stops": [ + [14, 0.5], + [20, 10] + ] + } + } + }, + { + "id": "bridge_motorway_link", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "bridge"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fc8", + "line-width": { + "base": 1.2, + "stops": [ + [12.5, 0], + [13, 1.5], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "bridge_service_track", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "service", "track"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fff", + "line-width": { + "base": 1.2, + "stops": [ + [15.5, 0], + [16, 2], + [20, 7.5] + ] + } + } + }, + { + "id": "bridge_link", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "link"], ["==", "brunnel", "bridge"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fea", + "line-width": { + "base": 1.2, + "stops": [ + [12.5, 0], + [13, 1.5], + [14, 2.5], + [20, 11.5] + ] + } + } + }, + { + "id": "bridge_street", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "minor"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fff", + "line-width": { + "base": 1.2, + "stops": [ + [13.5, 0], + [14, 2.5], + [20, 18] + ] + } + } + }, + { + "id": "bridge_secondary_tertiary", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "secondary", "tertiary"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fea", + "line-width": { + "base": 1.2, + "stops": [ + [6.5, 0], + [7, 0.5], + [20, 10] + ] + } + } + }, + { + "id": "bridge_trunk_primary", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "primary", "trunk"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fea", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0], + [7, 1], + [20, 18] + ] + } + } + }, + { + "id": "bridge_motorway", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#fc8", + "line-width": { + "base": 1.2, + "stops": [ + [5, 0], + [7, 1], + [20, 18] + ] + } + } + }, + { + "id": "bridge_major_rail", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "bridge"]], + "paint": { + "line-color": "#bbb", + "line-width": { + "base": 1.4, + "stops": [ + [14, 0.4], + [15, 0.75], + [20, 2] + ] + } + } + }, + { + "id": "bridge_major_rail_hatching", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "bridge"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": { + "base": 1.4, + "stops": [ + [14.5, 0], + [15, 3], + [20, 8] + ] + } + } + }, + { + "id": "bridge_transit_rail", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "transit"], ["==", "brunnel", "bridge"]], + "paint": { + "line-color": "#bbb", + "line-width": { + "base": 1.4, + "stops": [ + [14, 0.4], + [15, 0.75], + [20, 2] + ] + } + } + }, + { + "id": "bridge_transit_rail_hatching", + "type": "line", + "source": "immich-map", + "source-layer": "transportation", + "filter": ["all", ["==", "class", "transit"], ["==", "brunnel", "bridge"]], + "paint": { + "line-color": "#bbb", + "line-dasharray": [0.2, 8], + "line-width": { + "base": 1.4, + "stops": [ + [14.5, 0], + [15, 3], + [20, 8] + ] + } + } + }, + { + "id": "building", + "type": "fill", + "source": "immich-map", + "source-layer": "building", + "minzoom": 13, + "maxzoom": 14, + "paint": { + "fill-color": "hsl(35, 8%, 85%)", + "fill-outline-color": { + "base": 1, + "stops": [ + [13, "hsla(35, 6%, 79%, 0.32)"], + [14, "hsl(35, 6%, 79%)"] + ] + } + } + }, + { + "id": "building-3d", + "type": "fill-extrusion", + "source": "immich-map", + "source-layer": "building", + "minzoom": 14, + "paint": { + "fill-extrusion-color": "hsl(35, 8%, 85%)", + "fill-extrusion-height": { + "property": "render_height", + "type": "identity" + }, + "fill-extrusion-base": { + "property": "render_min_height", + "type": "identity" + }, + "fill-extrusion-opacity": 0.8 + } + }, + { + "id": "boundary_state", + "type": "line", + "source": "immich-map", + "source-layer": "boundary", + "paint": { "line-color": "rgba(185, 185, 185, 0.58)" } + }, + { + "id": "boundary_3", + "type": "line", + "source": "immich-map", + "source-layer": "boundary", + "minzoom": 8, + "filter": ["all", ["in", "admin_level", 3, 4]], + "layout": { "line-join": "round" }, + "paint": { + "line-color": "#9e9cab", + "line-dasharray": [5, 1], + "line-width": { + "base": 1, + "stops": [ + [4, 0.4], + [5, 1], + [12, 1.8] + ] + } + } + }, + { + "id": "boundary_2_z0-4", + "type": "line", + "source": "immich-map", + "source-layer": "boundary", + "maxzoom": 5, + "filter": ["all", ["==", "admin_level", 2], ["!has", "claimed_by"]], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "hsl(240, 50%, 60%)", + "line-opacity": { + "base": 1, + "stops": [ + [0, 0.4], + [4, 1] + ] + }, + "line-width": { + "base": 1, + "stops": [ + [3, 1], + [5, 1.2], + [12, 3] + ] + } + } + }, + { + "id": "boundary_2_z5-", + "type": "line", + "source": "immich-map", + "source-layer": "boundary", + "minzoom": 5, + "filter": ["all", ["==", "admin_level", 2]], + "layout": { "line-cap": "round", "line-join": "round" }, + "paint": { + "line-color": "hsl(248, 1%, 41%)", + "line-opacity": { + "base": 1, + "stops": [ + [0, 0.4], + [4, 1] + ] + }, + "line-width": { + "base": 1, + "stops": [ + [3, 1], + [5, 1.2], + [12, 3] + ] + } + } + }, + { + "id": "water_name_line", + "type": "symbol", + "source": "immich-map", + "source-layer": "waterway", + "filter": ["all", ["==", "$type", "LineString"]], + "layout": { + "text-field": "{name}", + "text-font": ["Open Sans Regular"], + "text-max-width": 5, + "text-size": 12, + "symbol-placement": "line" + }, + "paint": { + "text-color": "#5d60be", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "water_name_point", + "type": "symbol", + "source": "immich-map", + "source-layer": "water_name", + "filter": ["==", "$type", "Point"], + "layout": { + "text-field": "{name}", + "text-font": ["Open Sans Regular"], + "text-max-width": 5, + "text-size": 12 + }, + "paint": { + "text-color": "#5d60be", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "poi_z16", + "type": "symbol", + "source": "immich-map", + "source-layer": "poi", + "minzoom": 16, + "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 20]], + "layout": { + "icon-image": "{class}_11", + "text-anchor": "top", + "text-field": "{name}", + "text-font": ["Open Sans Italic"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-size": 12 + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "poi_z15", + "type": "symbol", + "source": "immich-map", + "source-layer": "poi", + "minzoom": 15, + "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 7], ["<", "rank", 20]], + "layout": { + "icon-image": "{class}_11", + "text-anchor": "top", + "text-field": "{name}", + "text-font": ["Open Sans Italic"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-size": 12 + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "poi_z14", + "type": "symbol", + "source": "immich-map", + "source-layer": "poi", + "minzoom": 14, + "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 1], ["<", "rank", 7]], + "layout": { + "icon-image": "{class}_11", + "text-anchor": "top", + "text-field": "{name}", + "text-font": ["Open Sans Italic"], + "text-max-width": 9, + "text-offset": [0, 0.6], + "text-size": 12 + }, + "paint": { + "text-color": "#666", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "poi_transit", + "type": "symbol", + "source": "immich-map", + "source-layer": "poi", + "filter": ["all", ["in", "class", "bus", "rail", "airport"]], + "layout": { + "icon-image": "{class}_11", + "text-anchor": "left", + "text-field": "{name_en}", + "text-font": ["Open Sans Italic"], + "text-max-width": 9, + "text-offset": [0.9, 0], + "text-size": 12 + }, + "paint": { + "text-color": "#4898ff", + "text-halo-blur": 0.5, + "text-halo-color": "#ffffff", + "text-halo-width": 1 + } + }, + { + "id": "road_label", + "type": "symbol", + "source": "immich-map", + "source-layer": "transportation_name", + "filter": ["all"], + "layout": { + "symbol-placement": "line", + "text-anchor": "center", + "text-field": "{name}", + "text-font": ["Open Sans Regular"], + "text-offset": [0, 0.15], + "text-size": { + "base": 1, + "stops": [ + [13, 12], + [14, 13] + ] + } + }, + "paint": { + "text-color": "#765", + "text-halo-blur": 0.5, + "text-halo-width": 1 + } + }, + { + "id": "road_shield", + "type": "symbol", + "source": "immich-map", + "source-layer": "transportation_name", + "minzoom": 7, + "filter": ["all", ["<=", "ref_length", 6]], + "layout": { + "icon-image": "default_{ref_length}", + "icon-rotation-alignment": "viewport", + "symbol-placement": { + "base": 1, + "stops": [ + [10, "point"], + [11, "line"] + ] + }, + "symbol-spacing": 500, + "text-field": "{ref}", + "text-font": ["Open Sans Regular"], + "text-offset": [0, 0.1], + "text-rotation-alignment": "viewport", + "text-size": 10, + "icon-size": 0.8 + } + }, + { + "id": "place_other", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "filter": ["all", ["in", "class", "hamlet", "island", "islet", "neighbourhood", "suburb", "quarter"]], + "layout": { + "text-field": "{name_en}", + "text-font": ["Open Sans Italic"], + "text-letter-spacing": 0.1, + "text-max-width": 9, + "text-size": { + "base": 1.2, + "stops": [ + [12, 10], + [15, 14] + ] + }, + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#633", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 1.2 + } + }, + { + "id": "place_village", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "filter": ["all", ["==", "class", "village"]], + "layout": { + "text-field": "{name_en}", + "text-font": ["Open Sans Regular"], + "text-max-width": 8, + "text-size": { + "base": 1.2, + "stops": [ + [10, 12], + [15, 22] + ] + } + }, + "paint": { + "text-color": "#333", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 1.2 + } + }, + { + "id": "place_town", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "filter": ["all", ["==", "class", "town"]], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [0, "dot_9"], + [8, ""] + ] + }, + "text-anchor": "bottom", + "text-field": "{name_en}", + "text-font": ["Open Sans Regular"], + "text-max-width": 8, + "text-offset": [0, 0], + "text-size": { + "base": 1.2, + "stops": [ + [7, 12], + [11, 16] + ] + } + }, + "paint": { + "text-color": "#333", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 1.2 + } + }, + { + "id": "place_city", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "minzoom": 5, + "filter": ["all", ["==", "class", "city"]], + "layout": { + "icon-image": { + "base": 1, + "stops": [ + [0, "dot_9"], + [8, ""] + ] + }, + "text-anchor": "bottom", + "text-field": "{name_en}", + "text-font": ["Open Sans Semibold"], + "text-max-width": 8, + "text-offset": [0, 0], + "text-size": { + "base": 1.2, + "stops": [ + [7, 14], + [11, 24] + ] + }, + "icon-allow-overlap": true, + "icon-optional": false + }, + "paint": { + "text-color": "#333", + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 1.2 + } + }, + { + "id": "state", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "maxzoom": 6, + "minzoom": 3.5, + "filter": ["all", ["==", "class", "state"]], + "layout": { + "text-field": "{name_en}", + "text-font": ["Open Sans Italic"], + "text-size": { + "stops": [ + [4, 11], + [6, 15] + ] + }, + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#633", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1 + } + }, + { + "id": "country_3", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "filter": ["all", [">=", "rank", 3], ["==", "class", "country"]], + "layout": { + "text-field": "{name_en}", + "text-font": ["Open Sans Italic"], + "text-max-width": 6.25, + "text-size": { + "stops": [ + [3, 11], + [7, 17] + ] + }, + "text-transform": "none" + }, + "paint": { + "text-color": "#334", + "text-halo-blur": 1, + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 1 + } + }, + { + "id": "country_2", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "filter": ["all", ["==", "rank", 2], ["==", "class", "country"]], + "layout": { + "text-field": "{name_en}", + "text-font": ["Open Sans Italic"], + "text-max-width": 6.25, + "text-size": { + "stops": [ + [2, 11], + [5, 17] + ] + }, + "text-transform": "none" + }, + "paint": { + "text-color": "#334", + "text-halo-blur": 1, + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 1 + } + }, + { + "id": "country_1", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "filter": ["all", ["==", "rank", 1], ["==", "class", "country"]], + "layout": { + "text-field": "{name_en}", + "text-font": ["Open Sans Italic"], + "text-max-width": 6.25, + "text-size": { + "stops": [ + [1, 11], + [4, 17] + ] + }, + "text-transform": "none" + }, + "paint": { + "text-color": "#334", + "text-halo-blur": 1, + "text-halo-color": "rgba(255,255,255,0.8)", + "text-halo-width": 1 + } + }, + { + "id": "continent", + "type": "symbol", + "source": "immich-map", + "source-layer": "place", + "maxzoom": 1, + "filter": ["all", ["==", "class", "continent"]], + "layout": { + "text-field": "{name_en}", + "text-font": ["Open Sans Italic"], + "text-size": 13, + "text-transform": "uppercase", + "text-justify": "center" + }, + "paint": { + "text-color": "#633", + "text-halo-color": "rgba(255,255,255,0.7)", + "text-halo-width": 1 + } + } + ], + "id": "immich-map-light" +} diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts index 414252778..862fafc32 100644 --- a/server/src/domain/access/access.core.ts +++ b/server/src/domain/access/access.core.ts @@ -1,5 +1,6 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { AuthUserDto } from '../auth'; +import { setDifference, setIsEqual, setUnion } from '../domain.util'; import { IAccessRepository } from '../repositories'; export enum Permission { @@ -40,6 +41,8 @@ export enum Permission { PERSON_READ = 'person.read', PERSON_WRITE = 'person.write', PERSON_MERGE = 'person.merge', + + PARTNER_UPDATE = 'partner.update', } let instance: AccessCore | null; @@ -66,82 +69,204 @@ export class AccessCore { return authUser; } + /** + * Check if user has access to all ids, for the given permission. + * Throws error if user does not have access to any of the ids. + */ async requirePermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) { - const hasAccess = await this.hasPermission(authUser, permission, ids); - if (!hasAccess) { + ids = Array.isArray(ids) ? ids : [ids]; + const allowedIds = await this.checkAccess(authUser, permission, ids); + if (!setIsEqual(new Set(ids), allowedIds)) { throw new BadRequestException(`Not found or no ${permission} access`); } } - async hasAny(authUser: AuthUserDto, permissions: Array<{ permission: Permission; id: string }>) { - for (const { permission, id } of permissions) { - const hasAccess = await this.hasPermission(authUser, permission, id); - if (hasAccess) { - return true; - } + /** + * Return ids that user has access to, for the given permission. + * Check is done for each id, and only allowed ids are returned. + * + * @returns Set + */ + async checkAccess(authUser: AuthUserDto, permission: Permission, ids: Set | string[]) { + const idSet = Array.isArray(ids) ? new Set(ids) : ids; + if (idSet.size === 0) { + return new Set(); } - return false; - } - - async hasPermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) { - ids = Array.isArray(ids) ? ids : [ids]; const isSharedLink = authUser.isPublicUser ?? false; - - for (const id of ids) { - const hasAccess = isSharedLink - ? await this.hasSharedLinkAccess(authUser, permission, id) - : await this.hasOtherAccess(authUser, permission, id); - if (!hasAccess) { - return false; - } - } - - return true; + return isSharedLink + ? await this.checkAccessSharedLink(authUser, permission, idSet) + : await this.checkAccessOther(authUser, permission, idSet); } - private async hasSharedLinkAccess(authUser: AuthUserDto, permission: Permission, id: string) { + private async checkAccessSharedLink(authUser: AuthUserDto, permission: Permission, ids: Set) { const sharedLinkId = authUser.sharedLinkId; if (!sharedLinkId) { - return false; + return new Set(); } switch (permission) { case Permission.ASSET_READ: - return this.repository.asset.hasSharedLinkAccess(sharedLinkId, id); + return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); case Permission.ASSET_VIEW: - return await this.repository.asset.hasSharedLinkAccess(sharedLinkId, id); + return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); case Permission.ASSET_DOWNLOAD: - return !!authUser.isAllowDownload && (await this.repository.asset.hasSharedLinkAccess(sharedLinkId, id)); + return !!authUser.isAllowDownload + ? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids) + : new Set(); case Permission.ASSET_UPLOAD: - return authUser.isAllowUpload; + return authUser.isAllowUpload ? ids : new Set(); case Permission.ASSET_SHARE: // TODO: fix this to not use authUser.id for shared link access control - return this.repository.asset.hasOwnerAccess(authUser.id, id); + return await this.repository.asset.checkOwnerAccess(authUser.id, ids); case Permission.ALBUM_READ: - return this.repository.album.hasSharedLinkAccess(sharedLinkId, id); + return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids); case Permission.ALBUM_DOWNLOAD: - return !!authUser.isAllowDownload && (await this.repository.album.hasSharedLinkAccess(sharedLinkId, id)); + return !!authUser.isAllowDownload + ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids) + : new Set(); default: - return false; + return new Set(); } } + private async checkAccessOther(authUser: AuthUserDto, permission: Permission, ids: Set) { + switch (permission) { + case Permission.ASSET_READ: { + const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids); + const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner)); + const isPartner = await this.repository.asset.checkPartnerAccess( + authUser.id, + setDifference(ids, isOwner, isAlbum), + ); + return setUnion(isOwner, isAlbum, isPartner); + } + + case Permission.ASSET_SHARE: { + const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids); + const isPartner = await this.repository.asset.checkPartnerAccess(authUser.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); + } + + case Permission.ASSET_VIEW: { + const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids); + const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner)); + const isPartner = await this.repository.asset.checkPartnerAccess( + authUser.id, + setDifference(ids, isOwner, isAlbum), + ); + return setUnion(isOwner, isAlbum, isPartner); + } + + case Permission.ASSET_DOWNLOAD: { + const isOwner = await this.repository.asset.checkOwnerAccess(authUser.id, ids); + const isAlbum = await this.repository.asset.checkAlbumAccess(authUser.id, setDifference(ids, isOwner)); + const isPartner = await this.repository.asset.checkPartnerAccess( + authUser.id, + setDifference(ids, isOwner, isAlbum), + ); + return setUnion(isOwner, isAlbum, isPartner); + } + + case Permission.ASSET_UPDATE: + return await this.repository.asset.checkOwnerAccess(authUser.id, ids); + + case Permission.ASSET_DELETE: + return await this.repository.asset.checkOwnerAccess(authUser.id, ids); + + case Permission.ASSET_RESTORE: + return await this.repository.asset.checkOwnerAccess(authUser.id, ids); + + case Permission.ALBUM_READ: { + const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids); + const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isShared); + } + + case Permission.ALBUM_UPDATE: + return await this.repository.album.checkOwnerAccess(authUser.id, ids); + + case Permission.ALBUM_DELETE: + return await this.repository.album.checkOwnerAccess(authUser.id, ids); + + case Permission.ALBUM_SHARE: + return await this.repository.album.checkOwnerAccess(authUser.id, ids); + + case Permission.ALBUM_DOWNLOAD: { + const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids); + const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isShared); + } + + case Permission.ALBUM_REMOVE_ASSET: + return await this.repository.album.checkOwnerAccess(authUser.id, ids); + + case Permission.ASSET_UPLOAD: + return await this.repository.library.checkOwnerAccess(authUser.id, ids); + + case Permission.ARCHIVE_READ: + return ids.has(authUser.id) ? new Set([authUser.id]) : new Set(); + + case Permission.AUTH_DEVICE_DELETE: + return await this.repository.authDevice.checkOwnerAccess(authUser.id, ids); + + case Permission.TIMELINE_READ: { + const isOwner = ids.has(authUser.id) ? new Set([authUser.id]) : new Set(); + const isPartner = await this.repository.timeline.checkPartnerAccess(authUser.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); + } + + case Permission.TIMELINE_DOWNLOAD: + return ids.has(authUser.id) ? new Set([authUser.id]) : new Set(); + + case Permission.LIBRARY_READ: { + const isOwner = await this.repository.library.checkOwnerAccess(authUser.id, ids); + const isPartner = await this.repository.library.checkPartnerAccess(authUser.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); + } + + case Permission.LIBRARY_UPDATE: + return await this.repository.library.checkOwnerAccess(authUser.id, ids); + + case Permission.LIBRARY_DELETE: + return await this.repository.library.checkOwnerAccess(authUser.id, ids); + + case Permission.PERSON_READ: + return await this.repository.person.checkOwnerAccess(authUser.id, ids); + + case Permission.PERSON_WRITE: + return await this.repository.person.checkOwnerAccess(authUser.id, ids); + + case Permission.PERSON_MERGE: + return await this.repository.person.checkOwnerAccess(authUser.id, ids); + + case Permission.PARTNER_UPDATE: + return await this.repository.partner.checkUpdateAccess(authUser.id, ids); + } + + const allowedIds = new Set(); + for (const id of ids) { + const hasAccess = await this.hasOtherAccess(authUser, permission, id); + if (hasAccess) { + allowedIds.add(id); + } + } + return allowedIds; + } + + // TODO: Migrate logic to checkAccessOther to evaluate permissions in bulk. private async hasOtherAccess(authUser: AuthUserDto, permission: Permission, id: string) { switch (permission) { // uses album id case Permission.ACTIVITY_CREATE: - return ( - (await this.repository.album.hasOwnerAccess(authUser.id, id)) || - (await this.repository.album.hasSharedAlbumAccess(authUser.id, id)) - ); + return await this.repository.activity.hasCreateAccess(authUser.id, id); // uses activity id case Permission.ACTIVITY_DELETE: @@ -150,101 +275,6 @@ export class AccessCore { (await this.repository.activity.hasAlbumOwnerAccess(authUser.id, id)) ); - case Permission.ASSET_READ: - return ( - (await this.repository.asset.hasOwnerAccess(authUser.id, id)) || - (await this.repository.asset.hasAlbumAccess(authUser.id, id)) || - (await this.repository.asset.hasPartnerAccess(authUser.id, id)) - ); - case Permission.ASSET_UPDATE: - return this.repository.asset.hasOwnerAccess(authUser.id, id); - - case Permission.ASSET_DELETE: - return this.repository.asset.hasOwnerAccess(authUser.id, id); - - case Permission.ASSET_RESTORE: - return this.repository.asset.hasOwnerAccess(authUser.id, id); - - case Permission.ASSET_SHARE: - return ( - (await this.repository.asset.hasOwnerAccess(authUser.id, id)) || - (await this.repository.asset.hasPartnerAccess(authUser.id, id)) - ); - - case Permission.ASSET_VIEW: - return ( - (await this.repository.asset.hasOwnerAccess(authUser.id, id)) || - (await this.repository.asset.hasAlbumAccess(authUser.id, id)) || - (await this.repository.asset.hasPartnerAccess(authUser.id, id)) - ); - - case Permission.ASSET_DOWNLOAD: - return ( - (await this.repository.asset.hasOwnerAccess(authUser.id, id)) || - (await this.repository.asset.hasAlbumAccess(authUser.id, id)) || - (await this.repository.asset.hasPartnerAccess(authUser.id, id)) - ); - - case Permission.ALBUM_READ: - return ( - (await this.repository.album.hasOwnerAccess(authUser.id, id)) || - (await this.repository.album.hasSharedAlbumAccess(authUser.id, id)) - ); - - case Permission.ALBUM_UPDATE: - return this.repository.album.hasOwnerAccess(authUser.id, id); - - case Permission.ALBUM_DELETE: - return this.repository.album.hasOwnerAccess(authUser.id, id); - - case Permission.ALBUM_SHARE: - return this.repository.album.hasOwnerAccess(authUser.id, id); - - case Permission.ALBUM_DOWNLOAD: - return ( - (await this.repository.album.hasOwnerAccess(authUser.id, id)) || - (await this.repository.album.hasSharedAlbumAccess(authUser.id, id)) - ); - - case Permission.ASSET_UPLOAD: - return this.repository.library.hasOwnerAccess(authUser.id, id); - - case Permission.ALBUM_REMOVE_ASSET: - return this.repository.album.hasOwnerAccess(authUser.id, id); - - case Permission.ARCHIVE_READ: - return authUser.id === id; - - case Permission.AUTH_DEVICE_DELETE: - return this.repository.authDevice.hasOwnerAccess(authUser.id, id); - - case Permission.TIMELINE_READ: - return authUser.id === id || (await this.repository.timeline.hasPartnerAccess(authUser.id, id)); - - case Permission.TIMELINE_DOWNLOAD: - return authUser.id === id; - - case Permission.LIBRARY_READ: - return ( - (await this.repository.library.hasOwnerAccess(authUser.id, id)) || - (await this.repository.library.hasPartnerAccess(authUser.id, id)) - ); - - case Permission.LIBRARY_UPDATE: - return this.repository.library.hasOwnerAccess(authUser.id, id); - - case Permission.LIBRARY_DELETE: - return this.repository.library.hasOwnerAccess(authUser.id, id); - - case Permission.PERSON_READ: - return this.repository.person.hasOwnerAccess(authUser.id, id); - - case Permission.PERSON_WRITE: - return this.repository.person.hasOwnerAccess(authUser.id, id); - - case Permission.PERSON_MERGE: - return this.repository.person.hasOwnerAccess(authUser.id, id); - default: return false; } diff --git a/server/src/domain/activity/activity.dto.ts b/server/src/domain/activity/activity.dto.ts index e1a163b81..a5a5bd3df 100644 --- a/server/src/domain/activity/activity.dto.ts +++ b/server/src/domain/activity/activity.dto.ts @@ -9,6 +9,11 @@ export enum ReactionType { LIKE = 'like', } +export enum ReactionLevel { + ALBUM = 'album', + ASSET = 'asset', +} + export type MaybeDuplicate = { duplicate: boolean; value: T }; export class ActivityResponseDto { @@ -39,6 +44,11 @@ export class ActivitySearchDto extends ActivityDto { @ApiProperty({ enumName: 'ReactionType', enum: ReactionType }) type?: ReactionType; + @IsEnum(ReactionLevel) + @Optional() + @ApiProperty({ enumName: 'ReactionLevel', enum: ReactionLevel }) + level?: ReactionLevel; + @ValidateUUID({ optional: true }) userId?: string; } diff --git a/server/src/domain/activity/activity.service.ts b/server/src/domain/activity/activity.service.ts index 44362f3fc..8b601558d 100644 --- a/server/src/domain/activity/activity.service.ts +++ b/server/src/domain/activity/activity.service.ts @@ -10,6 +10,7 @@ import { ActivitySearchDto, ActivityStatisticsResponseDto, MaybeDuplicate, + ReactionLevel, ReactionType, mapActivity, } from './activity.dto'; @@ -30,7 +31,7 @@ export class ActivityService { const activities = await this.repository.search({ userId: dto.userId, albumId: dto.albumId, - assetId: dto.assetId, + assetId: dto.level === ReactionLevel.ALBUM ? null : dto.assetId, isLiked: dto.type && dto.type === ReactionType.LIKE, }); @@ -54,11 +55,12 @@ export class ActivityService { let activity: ActivityEntity | null = null; let duplicate = false; - if (dto.type === 'like') { + if (dto.type === ReactionType.LIKE) { delete dto.comment; [activity] = await this.repository.search({ ...common, - isGlobal: !dto.assetId, + // `null` will search for an album like + assetId: dto.assetId ?? null, isLiked: true, }); duplicate = !!activity; diff --git a/server/src/domain/activity/activity.spec.ts b/server/src/domain/activity/activity.spec.ts index 968f7421a..659718bed 100644 --- a/server/src/domain/activity/activity.spec.ts +++ b/server/src/domain/activity/activity.spec.ts @@ -24,7 +24,7 @@ describe(ActivityService.name, () => { describe('getAll', () => { it('should get all', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); activityMock.search.mockResolvedValue([]); await expect(sut.getAll(authStub.admin, { assetId: 'asset-id', albumId: 'album-id' })).resolves.toEqual([]); @@ -37,7 +37,7 @@ describe(ActivityService.name, () => { }); it('should filter by type=like', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); activityMock.search.mockResolvedValue([]); await expect( @@ -52,7 +52,7 @@ describe(ActivityService.name, () => { }); it('should filter by type=comment', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); activityMock.search.mockResolvedValue([]); await expect( @@ -70,7 +70,7 @@ describe(ActivityService.name, () => { describe('getStatistics', () => { it('should get the comment count', async () => { activityMock.getStatistics.mockResolvedValue(1); - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([activityStub.oneComment.albumId])); await expect( sut.getStatistics(authStub.admin, { assetId: 'asset-id', @@ -82,7 +82,6 @@ describe(ActivityService.name, () => { describe('addComment', () => { it('should require access to the album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); await expect( sut.create(authStub.admin, { albumId: 'album-id', @@ -94,7 +93,7 @@ describe(ActivityService.name, () => { }); it('should create a comment', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.activity.hasCreateAccess.mockResolvedValue(true); activityMock.create.mockResolvedValue(activityStub.oneComment); await sut.create(authStub.admin, { @@ -113,8 +112,23 @@ describe(ActivityService.name, () => { }); }); + it('should fail because activity is disabled for the album', async () => { + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); + accessMock.activity.hasCreateAccess.mockResolvedValue(false); + activityMock.create.mockResolvedValue(activityStub.oneComment); + + await expect( + sut.create(authStub.admin, { + albumId: 'album-id', + assetId: 'asset-id', + type: ReactionType.COMMENT, + comment: 'comment', + }), + ).rejects.toBeInstanceOf(BadRequestException); + }); + it('should create a like', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.activity.hasCreateAccess.mockResolvedValue(true); activityMock.create.mockResolvedValue(activityStub.liked); activityMock.search.mockResolvedValue([]); @@ -133,7 +147,8 @@ describe(ActivityService.name, () => { }); it('should skip if like exists', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); + accessMock.activity.hasCreateAccess.mockResolvedValue(true); activityMock.search.mockResolvedValue([activityStub.liked]); await sut.create(authStub.admin, { diff --git a/server/src/domain/album/album-response.dto.ts b/server/src/domain/album/album-response.dto.ts index b426bc37d..671922408 100644 --- a/server/src/domain/album/album-response.dto.ts +++ b/server/src/domain/album/album-response.dto.ts @@ -21,6 +21,7 @@ export class AlbumResponseDto { lastModifiedAssetTimestamp?: Date; startDate?: Date; endDate?: Date; + isActivityEnabled!: boolean; } export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumResponseDto => { @@ -61,6 +62,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons endDate, assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)), assetCount: entity.assets?.length || 0, + isActivityEnabled: entity.isActivityEnabled, }; }; diff --git a/server/src/domain/album/album.service.spec.ts b/server/src/domain/album/album.service.spec.ts index a93cb0ad1..9a4614c79 100644 --- a/server/src/domain/album/album.service.spec.ts +++ b/server/src/domain/album/album.service.spec.ts @@ -58,9 +58,9 @@ describe(AlbumService.name, () => { describe('getAll', () => { it('gets list of albums for auth user', async () => { albumMock.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]); - albumMock.getAssetCountForIds.mockResolvedValue([ - { albumId: albumStub.empty.id, assetCount: 0 }, - { albumId: albumStub.sharedWithUser.id, assetCount: 0 }, + albumMock.getMetadataForIds.mockResolvedValue([ + { albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined }, + { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined }, ]); albumMock.getInvalidThumbnail.mockResolvedValue([]); @@ -72,7 +72,14 @@ describe(AlbumService.name, () => { it('gets list of albums that have a specific asset', async () => { albumMock.getByAssetId.mockResolvedValue([albumStub.oneAsset]); - albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]); + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.oneAsset.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, + ]); albumMock.getInvalidThumbnail.mockResolvedValue([]); const result = await sut.getAll(authStub.admin, { assetId: albumStub.oneAsset.id }); @@ -83,7 +90,9 @@ describe(AlbumService.name, () => { it('gets list of albums that are shared', async () => { albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]); - albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.sharedWithUser.id, assetCount: 0 }]); + albumMock.getMetadataForIds.mockResolvedValue([ + { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined }, + ]); albumMock.getInvalidThumbnail.mockResolvedValue([]); const result = await sut.getAll(authStub.admin, { shared: true }); @@ -94,7 +103,9 @@ describe(AlbumService.name, () => { it('gets list of albums that are NOT shared', async () => { albumMock.getNotShared.mockResolvedValue([albumStub.empty]); - albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.empty.id, assetCount: 0 }]); + albumMock.getMetadataForIds.mockResolvedValue([ + { albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined }, + ]); albumMock.getInvalidThumbnail.mockResolvedValue([]); const result = await sut.getAll(authStub.admin, { shared: false }); @@ -106,7 +117,14 @@ describe(AlbumService.name, () => { it('counts assets correctly', async () => { albumMock.getOwned.mockResolvedValue([albumStub.oneAsset]); - albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]); + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.oneAsset.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, + ]); albumMock.getInvalidThumbnail.mockResolvedValue([]); const result = await sut.getAll(authStub.admin, {}); @@ -118,8 +136,13 @@ describe(AlbumService.name, () => { it('updates the album thumbnail by listing all albums', async () => { albumMock.getOwned.mockResolvedValue([albumStub.oneAssetInvalidThumbnail]); - albumMock.getAssetCountForIds.mockResolvedValue([ - { albumId: albumStub.oneAssetInvalidThumbnail.id, assetCount: 1 }, + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.oneAssetInvalidThumbnail.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, ]); albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]); albumMock.update.mockResolvedValue(albumStub.oneAssetValidThumbnail); @@ -134,8 +157,13 @@ describe(AlbumService.name, () => { it('removes the thumbnail for an empty album', async () => { albumMock.getOwned.mockResolvedValue([albumStub.emptyWithInvalidThumbnail]); - albumMock.getAssetCountForIds.mockResolvedValue([ - { albumId: albumStub.emptyWithInvalidThumbnail.id, assetCount: 1 }, + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.emptyWithInvalidThumbnail.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, ]); albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]); albumMock.update.mockResolvedValue(albumStub.emptyWithValidThumbnail); @@ -204,7 +232,6 @@ describe(AlbumService.name, () => { }); it('should prevent updating a not owned album (shared with auth user)', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); await expect( sut.update(authStub.admin, albumStub.sharedWithAdmin.id, { albumName: 'new album name', @@ -213,7 +240,7 @@ describe(AlbumService.name, () => { }); it('should require a valid thumbnail asset id', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-4'])); albumMock.getById.mockResolvedValue(albumStub.oneAsset); albumMock.update.mockResolvedValue(albumStub.oneAsset); albumMock.hasAsset.mockResolvedValue(false); @@ -229,7 +256,7 @@ describe(AlbumService.name, () => { }); it('should allow the owner to update the album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-4'])); albumMock.getById.mockResolvedValue(albumStub.oneAsset); albumMock.update.mockResolvedValue(albumStub.oneAsset); @@ -252,7 +279,7 @@ describe(AlbumService.name, () => { describe('delete', () => { it('should throw an error for an album not found', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); albumMock.getById.mockResolvedValue(null); await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf( @@ -263,7 +290,6 @@ describe(AlbumService.name, () => { }); it('should not let a shared user delete the album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf( @@ -274,7 +300,7 @@ describe(AlbumService.name, () => { }); it('should let the owner delete an album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.empty.id])); albumMock.getById.mockResolvedValue(albumStub.empty); await sut.delete(authStub.admin, albumStub.empty.id); @@ -286,7 +312,6 @@ describe(AlbumService.name, () => { describe('addUsers', () => { it('should throw an error if the auth user is not the owner', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); await expect( sut.addUsers(authStub.admin, albumStub.sharedWithAdmin.id, { sharedUserIds: ['user-1'] }), ).rejects.toBeInstanceOf(BadRequestException); @@ -294,7 +319,7 @@ describe(AlbumService.name, () => { }); it('should throw an error if the userId is already added', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); await expect( sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.admin.id] }), @@ -303,7 +328,7 @@ describe(AlbumService.name, () => { }); it('should throw an error if the userId does not exist', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); userMock.get.mockResolvedValue(null); await expect( @@ -313,7 +338,7 @@ describe(AlbumService.name, () => { }); it('should add valid shared users', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin)); albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin); userMock.get.mockResolvedValue(userStub.user2); @@ -328,14 +353,14 @@ describe(AlbumService.name, () => { describe('removeUser', () => { it('should require a valid album id', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1'])); albumMock.getById.mockResolvedValue(null); await expect(sut.removeUser(authStub.admin, 'album-1', 'user-1')).rejects.toBeInstanceOf(BadRequestException); expect(albumMock.update).not.toHaveBeenCalled(); }); it('should remove a shared user from an owned album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithUser.id])); albumMock.getById.mockResolvedValue(albumStub.sharedWithUser); await expect( @@ -352,7 +377,6 @@ describe(AlbumService.name, () => { }); it('should prevent removing a shared user from a not-owned album (shared with auth user)', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); albumMock.getById.mockResolvedValue(albumStub.sharedWithMultiple); await expect( @@ -360,7 +384,10 @@ describe(AlbumService.name, () => { ).rejects.toBeInstanceOf(BadRequestException); expect(albumMock.update).not.toHaveBeenCalled(); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.user1.id, albumStub.sharedWithMultiple.id); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith( + authStub.user1.id, + new Set([albumStub.sharedWithMultiple.id]), + ); }); it('should allow a shared user to remove themselves', async () => { @@ -413,52 +440,76 @@ describe(AlbumService.name, () => { describe('getAlbumInfo', () => { it('should get a shared album', async () => { albumMock.getById.mockResolvedValue(albumStub.oneAsset); - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id])); + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.oneAsset.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, + ]); await sut.get(authStub.admin, albumStub.oneAsset.id, {}); expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id, { withAssets: true }); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, albumStub.oneAsset.id); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith( + authStub.admin.id, + new Set([albumStub.oneAsset.id]), + ); }); it('should get a shared album via a shared link', async () => { albumMock.getById.mockResolvedValue(albumStub.oneAsset); - accessMock.album.hasSharedLinkAccess.mockResolvedValue(true); + accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123'])); + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.oneAsset.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, + ]); await sut.get(authStub.adminSharedLink, 'album-123', {}); expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true }); - expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith( + expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith( authStub.adminSharedLink.sharedLinkId, - 'album-123', + new Set(['album-123']), ); }); it('should get a shared album via shared with user', async () => { albumMock.getById.mockResolvedValue(albumStub.oneAsset); - accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true); + accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123'])); + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.oneAsset.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, + ]); await sut.get(authStub.user1, 'album-123', {}); expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true }); - expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalledWith(authStub.user1.id, 'album-123'); + expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(authStub.user1.id, new Set(['album-123'])); }); it('should throw an error for no access', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); - accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false); - await expect(sut.get(authStub.admin, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-123'); - expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-123'); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-123'])); + expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-123'])); }); }); describe('addAssets', () => { it('should allow the owner to add assets', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); albumMock.getAssetIds.mockResolvedValueOnce(new Set()); @@ -482,8 +533,8 @@ describe(AlbumService.name, () => { }); it('should not set the thumbnail if the album has one already', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); albumMock.getById.mockResolvedValue(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' })); albumMock.getAssetIds.mockResolvedValueOnce(new Set()); @@ -500,9 +551,8 @@ describe(AlbumService.name, () => { }); it('should allow a shared user to add assets', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); - accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true); - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser)); albumMock.getAssetIds.mockResolvedValueOnce(new Set()); @@ -526,10 +576,8 @@ describe(AlbumService.name, () => { }); it('should allow a shared link user to add assets', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); - accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false); - accessMock.album.hasSharedLinkAccess.mockResolvedValue(true); - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); albumMock.getAssetIds.mockResolvedValueOnce(new Set()); @@ -551,16 +599,15 @@ describe(AlbumService.name, () => { assetIds: ['asset-1', 'asset-2', 'asset-3'], }); - expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith( + expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith( authStub.adminSharedLink.sharedLinkId, - 'album-123', + new Set(['album-123']), ); }); it('should allow adding assets shared via partner sharing', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); - accessMock.asset.hasOwnerAccess.mockResolvedValue(false); - accessMock.asset.hasPartnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); + accessMock.asset.checkPartnerAccess.mockResolvedValue(new Set(['asset-1'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); albumMock.getAssetIds.mockResolvedValueOnce(new Set()); @@ -573,12 +620,12 @@ describe(AlbumService.name, () => { updatedAt: expect.any(Date), albumThumbnailAssetId: 'asset-1', }); - expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1'); + expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1'])); }); it('should skip duplicate assets', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id'])); @@ -590,9 +637,7 @@ describe(AlbumService.name, () => { }); it('should skip assets not shared with user', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); - accessMock.asset.hasOwnerAccess.mockResolvedValue(false); - accessMock.asset.hasPartnerAccess.mockResolvedValue(false); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); albumMock.getById.mockResolvedValue(albumStub.oneAsset); albumMock.getAssetIds.mockResolvedValueOnce(new Set()); @@ -600,38 +645,36 @@ describe(AlbumService.name, () => { { success: false, id: 'asset-1', error: BulkIdErrorReason.NO_PERMISSION }, ]); - expect(accessMock.asset.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1'); - expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1'); + expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1'])); + expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1'])); }); it('should not allow unauthorized access to the album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); - accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false); albumMock.getById.mockResolvedValue(albumStub.oneAsset); await expect( sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), ).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalled(); - expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalled(); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalled(); + expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalled(); }); it('should not allow unauthorized shared link access to the album', async () => { - accessMock.album.hasSharedLinkAccess.mockResolvedValue(false); albumMock.getById.mockResolvedValue(albumStub.oneAsset); await expect( sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), ).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalled(); + expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalled(); }); }); describe('removeAssets', () => { it('should allow the owner to remove assets', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123'])); + accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-id'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id'])); @@ -644,7 +687,7 @@ describe(AlbumService.name, () => { }); it('should skip assets not in the album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.empty)); albumMock.getAssetIds.mockResolvedValueOnce(new Set()); @@ -656,7 +699,7 @@ describe(AlbumService.name, () => { }); it('should skip assets without user permission to remove', async () => { - accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true); + accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id'])); @@ -672,7 +715,8 @@ describe(AlbumService.name, () => { }); it('should reset the thumbnail if it is removed', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123'])); + accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-id'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets)); albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id'])); diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts index b8e789943..308735d43 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/domain/album/album.service.ts @@ -3,8 +3,10 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { AccessCore, Permission } from '../access'; import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from '../asset'; import { AuthUserDto } from '../auth'; +import { setUnion } from '../domain.util'; import { JobName } from '../job'; import { + AlbumAssetCount, AlbumInfoOptions, IAccessRepository, IAlbumRepository, @@ -68,11 +70,19 @@ export class AlbumService { // Get asset count for each album. Then map the result to an object: // { [albumId]: assetCount } - const albumsAssetCount = await this.albumRepository.getAssetCountForIds(albums.map((album) => album.id)); - const albumsAssetCountObj = albumsAssetCount.reduce((obj: Record, { albumId, assetCount }) => { - obj[albumId] = assetCount; - return obj; - }, {}); + const albumMetadataForIds = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id)); + const albumMetadataForIdsObj: Record = albumMetadataForIds.reduce( + (obj: Record, { albumId, assetCount, startDate, endDate }) => { + obj[albumId] = { + albumId, + assetCount, + startDate, + endDate, + }; + return obj; + }, + {}, + ); return Promise.all( albums.map(async (album) => { @@ -80,17 +90,28 @@ export class AlbumService { return { ...mapAlbumWithoutAssets(album), sharedLinks: undefined, - assetCount: albumsAssetCountObj[album.id], + startDate: albumMetadataForIdsObj[album.id].startDate, + endDate: albumMetadataForIdsObj[album.id].endDate, + assetCount: albumMetadataForIdsObj[album.id].assetCount, lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt, }; }), ); } - async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto) { + async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto): Promise { await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); await this.albumRepository.updateThumbnails(); - return mapAlbum(await this.findOrFail(id, { withAssets: true }), !dto.withoutAssets); + const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; + const album = await this.findOrFail(id, { withAssets }); + const [albumMetadataForIds] = await this.albumRepository.getMetadataForIds([album.id]); + + return { + ...mapAlbum(album, withAssets), + startDate: albumMetadataForIds.startDate, + endDate: albumMetadataForIds.endDate, + assetCount: albumMetadataForIds.assetCount, + }; } async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise { @@ -125,12 +146,12 @@ export class AlbumService { throw new BadRequestException('Invalid album thumbnail'); } } - const updatedAlbum = await this.albumRepository.update({ id: album.id, albumName: dto.albumName, description: dto.description, albumThumbnailAssetId: dto.albumThumbnailAssetId, + isActivityEnabled: dto.isActivityEnabled, }); await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [updatedAlbum.id] } }); @@ -153,6 +174,8 @@ export class AlbumService { await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids); + const notPresentAssetIds = dto.ids.filter((id) => !existingAssetIds.has(id)); + const allowedAssetIds = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, notPresentAssetIds); const results: BulkIdResponseDto[] = []; for (const assetId of dto.ids) { @@ -162,7 +185,7 @@ export class AlbumService { continue; } - const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, assetId); + const hasAccess = allowedAssetIds.has(assetId); if (!hasAccess) { results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION }); continue; @@ -190,6 +213,9 @@ export class AlbumService { await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids); + const canRemove = await this.access.checkAccess(authUser, Permission.ALBUM_REMOVE_ASSET, existingAssetIds); + const canShare = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, existingAssetIds); + const allowedAssetIds = setUnion(canRemove, canShare); const results: BulkIdResponseDto[] = []; for (const assetId of dto.ids) { @@ -199,10 +225,7 @@ export class AlbumService { continue; } - const hasAccess = await this.access.hasAny(authUser, [ - { permission: Permission.ALBUM_REMOVE_ASSET, id: assetId }, - { permission: Permission.ASSET_SHARE, id: assetId }, - ]); + const hasAccess = allowedAssetIds.has(assetId); if (!hasAccess) { results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION }); continue; diff --git a/server/src/domain/album/dto/album-update.dto.ts b/server/src/domain/album/dto/album-update.dto.ts index f574f2c23..3b1858ba1 100644 --- a/server/src/domain/album/dto/album-update.dto.ts +++ b/server/src/domain/album/dto/album-update.dto.ts @@ -1,4 +1,4 @@ -import { IsString } from 'class-validator'; +import { IsBoolean, IsString } from 'class-validator'; import { Optional, ValidateUUID } from '../../domain.util'; export class UpdateAlbumDto { @@ -12,4 +12,8 @@ export class UpdateAlbumDto { @ValidateUUID({ optional: true }) albumThumbnailAssetId?: string; + + @Optional() + @IsBoolean() + isActivityEnabled?: boolean; } diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index 7b93935f6..e4052fb34 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -10,6 +10,7 @@ import { newCommunicationRepositoryMock, newCryptoRepositoryMock, newJobRepositoryMock, + newPartnerRepositoryMock, newStorageRepositoryMock, newSystemConfigRepositoryMock, } from '@test'; @@ -23,6 +24,7 @@ import { ICommunicationRepository, ICryptoRepository, IJobRepository, + IPartnerRepository, IStorageRepository, ISystemConfigRepository, JobItem, @@ -164,6 +166,7 @@ describe(AssetService.name, () => { let storageMock: jest.Mocked; let communicationMock: jest.Mocked; let configMock: jest.Mocked; + let partnerMock: jest.Mocked; it('should work', () => { expect(sut).toBeDefined(); @@ -177,7 +180,18 @@ describe(AssetService.name, () => { jobMock = newJobRepositoryMock(); storageMock = newStorageRepositoryMock(); configMock = newSystemConfigRepositoryMock(); - sut = new AssetService(accessMock, assetMock, cryptoMock, jobMock, configMock, storageMock, communicationMock); + partnerMock = newPartnerRepositoryMock(); + + sut = new AssetService( + accessMock, + assetMock, + cryptoMock, + jobMock, + configMock, + storageMock, + communicationMock, + partnerMock, + ); when(assetMock.getById) .calledWith(assetStub.livePhotoStillAsset.id) @@ -327,20 +341,20 @@ describe(AssetService.name, () => { size: TimeBucketSize.DAY, }), ).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }])); - expect(assetMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.DAY, userId: authStub.admin.id }); + expect(assetMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.DAY, userIds: [authStub.admin.id] }); }); }); describe('getTimeBucket', () => { it('should return the assets for a album time bucket if user has album.read', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); assetMock.getTimeBucket.mockResolvedValue([assetStub.image]); await expect( sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }), ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-id'); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-id'])); expect(assetMock.getTimeBucket).toBeCalledWith('bucket', { size: TimeBucketSize.DAY, timeBucket: 'bucket', @@ -363,7 +377,7 @@ describe(AssetService.name, () => { size: TimeBucketSize.DAY, timeBucket: 'bucket', isArchived: true, - userId: authStub.admin.id, + userIds: [authStub.admin.id], }); }); @@ -380,26 +394,78 @@ describe(AssetService.name, () => { expect(assetMock.getTimeBucket).toBeCalledWith('bucket', { size: TimeBucketSize.DAY, timeBucket: 'bucket', - userId: authStub.admin.id, + userIds: [authStub.admin.id], }); }); + + it('should throw an error if withParners is true and isArchived true or undefined', async () => { + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isArchived: true, + withPartners: true, + userId: authStub.admin.id, + }), + ).rejects.toThrowError(BadRequestException); + + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isArchived: undefined, + withPartners: true, + userId: authStub.admin.id, + }), + ).rejects.toThrowError(BadRequestException); + }); + + it('should throw an error if withParners is true and isFavorite is either true or false', async () => { + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isFavorite: true, + withPartners: true, + userId: authStub.admin.id, + }), + ).rejects.toThrowError(BadRequestException); + + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isFavorite: false, + withPartners: true, + userId: authStub.admin.id, + }), + ).rejects.toThrowError(BadRequestException); + }); + + it('should throw an error if withParners is true and isTrash is true', async () => { + await expect( + sut.getTimeBucket(authStub.admin, { + size: TimeBucketSize.DAY, + timeBucket: 'bucket', + isTrashed: true, + withPartners: true, + userId: authStub.admin.id, + }), + ).rejects.toThrowError(BadRequestException); + }); }); describe('downloadFile', () => { it('should require the asset.download permission', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(false); - accessMock.asset.hasAlbumAccess.mockResolvedValue(false); - accessMock.asset.hasPartnerAccess.mockResolvedValue(false); - await expect(sut.downloadFile(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.asset.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1'); - expect(accessMock.asset.hasAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1'); - expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1'); + expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1'])); + expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1'])); + expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['asset-1'])); }); it('should throw an error if the asset is not found', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); assetMock.getByIds.mockResolvedValue([]); await expect(sut.downloadFile(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(BadRequestException); @@ -410,7 +476,7 @@ describe(AssetService.name, () => { it('should download a file', async () => { const stream = new Readable(); - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); assetMock.getByIds.mockResolvedValue([assetStub.image]); storageMock.createReadStream.mockResolvedValue({ stream }); @@ -426,7 +492,7 @@ describe(AssetService.name, () => { stream: new Readable(), }; - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); assetMock.getByIds.mockResolvedValue([assetStub.noResizePath, assetStub.noWebpPath]); storageMock.createZipStream.mockReturnValue(archiveMock); @@ -446,7 +512,7 @@ describe(AssetService.name, () => { stream: new Readable(), }; - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); assetMock.getByIds.mockResolvedValue([assetStub.noResizePath, assetStub.noResizePath]); storageMock.createZipStream.mockReturnValue(archiveMock); @@ -466,7 +532,7 @@ describe(AssetService.name, () => { }); it('should return a list of archives (assetIds)', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); assetMock.getByIds.mockResolvedValue([assetStub.image, assetStub.video]); const assetIds = ['asset-1', 'asset-2']; @@ -476,7 +542,7 @@ describe(AssetService.name, () => { }); it('should return a list of archives (albumId)', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1'])); assetMock.getByAlbumId.mockResolvedValue({ items: [assetStub.image, assetStub.video], hasNextPage: false, @@ -484,12 +550,12 @@ describe(AssetService.name, () => { await expect(sut.getDownloadInfo(authStub.admin, { albumId: 'album-1' })).resolves.toEqual(downloadResponse); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-1'); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-1'])); expect(assetMock.getByAlbumId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, 'album-1'); }); it('should return a list of archives (userId)', async () => { - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id])); assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image, assetStub.video], hasNextPage: false, @@ -505,7 +571,7 @@ describe(AssetService.name, () => { }); it('should split archives by size', async () => { - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id])); assetMock.getByUserId.mockResolvedValue({ items: [ @@ -532,7 +598,9 @@ describe(AssetService.name, () => { }); it('should include the video portion of a live photo', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + const assetIds = [assetStub.livePhotoStillAsset.id]; + + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds)); when(assetMock.getByIds) .calledWith([assetStub.livePhotoStillAsset.id]) .mockResolvedValue([assetStub.livePhotoStillAsset]); @@ -540,7 +608,6 @@ describe(AssetService.name, () => { .calledWith([assetStub.livePhotoMotionAsset.id]) .mockResolvedValue([assetStub.livePhotoMotionAsset]); - const assetIds = [assetStub.livePhotoStillAsset.id]; await expect(sut.getDownloadInfo(authStub.admin, { assetIds })).resolves.toEqual({ totalSize: 125_000, archives: [ @@ -581,7 +648,6 @@ describe(AssetService.name, () => { describe('update', () => { it('should require asset write access for the id', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(false); await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf( BadRequestException, ); @@ -589,14 +655,14 @@ describe(AssetService.name, () => { }); it('should update the asset', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); assetMock.save.mockResolvedValue(assetStub.image); await sut.update(authStub.admin, 'asset-1', { isFavorite: true }); expect(assetMock.save).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true }); }); it('should update the exif description', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); assetMock.save.mockResolvedValue(assetStub.image); await sut.update(authStub.admin, 'asset-1', { description: 'Test description' }); expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' }); @@ -605,7 +671,6 @@ describe(AssetService.name, () => { describe('updateAll', () => { it('should require asset write access for all ids', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(false); await expect( sut.updateAll(authStub.admin, { ids: ['asset-1'], @@ -615,7 +680,7 @@ describe(AssetService.name, () => { }); it('should update all assets', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true }); expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true }); }); @@ -623,8 +688,7 @@ describe(AssetService.name, () => { /// Stack related it('should require asset update access for parent', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); - when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'parent').mockResolvedValue(false); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); await expect( sut.updateAll(authStub.user1, { ids: ['asset-1'], @@ -634,7 +698,7 @@ describe(AssetService.name, () => { }); it('should update parent asset when children are added', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['parent'])); await sut.updateAll(authStub.user1, { ids: [], stackParentId: 'parent', @@ -643,7 +707,7 @@ describe(AssetService.name, () => { }); it('should update parent asset when children are removed', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['child-1'])); assetMock.getByIds.mockResolvedValue([{ id: 'child-1', stackParentId: 'parent' } as AssetEntity]); await sut.updateAll(authStub.user1, { @@ -654,7 +718,8 @@ describe(AssetService.name, () => { }); it('update parentId for new children', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['child-1', 'child-2'])); + accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent'])); await sut.updateAll(authStub.user1, { stackParentId: 'parent', ids: ['child-1', 'child-2'], @@ -664,7 +729,7 @@ describe(AssetService.name, () => { }); it('nullify parentId for remove children', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['child-1', 'child-2'])); await sut.updateAll(authStub.user1, { removeParent: true, ids: ['child-1', 'child-2'], @@ -674,7 +739,8 @@ describe(AssetService.name, () => { }); it('merge stacks if new child has children', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['child-1'])); + accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent'])); assetMock.getByIds.mockResolvedValue([ { id: 'child-1', stack: [{ id: 'child-2' } as AssetEntity] } as AssetEntity, ]); @@ -688,7 +754,9 @@ describe(AssetService.name, () => { }); it('should send ws asset update event', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-1'])); + accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent'])); + await sut.updateAll(authStub.user1, { ids: ['asset-1'], stackParentId: 'parent', @@ -702,7 +770,6 @@ describe(AssetService.name, () => { describe('deleteAll', () => { it('should require asset delete access for all ids', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(false); await expect( sut.deleteAll(authStub.user1, { ids: ['asset-1'], @@ -711,7 +778,7 @@ describe(AssetService.name, () => { }); it('should force delete a batch of assets', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2'])); await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true }); @@ -722,7 +789,7 @@ describe(AssetService.name, () => { }); it('should soft delete a batch of assets', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2'])); await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: false }); @@ -740,7 +807,6 @@ describe(AssetService.name, () => { describe('restoreAll', () => { it('should require asset restore access for all ids', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(false); await expect( sut.deleteAll(authStub.user1, { ids: ['asset-1'], @@ -749,7 +815,7 @@ describe(AssetService.name, () => { }); it('should restore a batch of assets', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2'])); await sut.restoreAll(authStub.user1, { ids: ['asset1', 'asset2'] }); @@ -914,19 +980,19 @@ describe(AssetService.name, () => { describe('run', () => { it('should run the refresh metadata job', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_METADATA }), expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.METADATA_EXTRACTION, data: { id: 'asset-1' } }); }); it('should run the refresh thumbnails job', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL }), expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1' } }); }); it('should run the transcode video', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.TRANSCODE_VIDEO }), expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.VIDEO_CONVERSION, data: { id: 'asset-1' } }); }); @@ -934,9 +1000,7 @@ describe(AssetService.name, () => { describe('updateStackParent', () => { it('should require asset update access for new parent', async () => { - when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'old').mockResolvedValue(true); - when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'new').mockResolvedValue(false); - accessMock.asset.hasOwnerAccess.mockResolvedValue(false); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['old'])); await expect( sut.updateStackParent(authStub.user1, { oldParentId: 'old', @@ -946,8 +1010,7 @@ describe(AssetService.name, () => { }); it('should require asset read access for old parent', async () => { - when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'old').mockResolvedValue(false); - when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'new').mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['new'])); await expect( sut.updateStackParent(authStub.user1, { oldParentId: 'old', @@ -957,7 +1020,9 @@ describe(AssetService.name, () => { }); it('make old parent the child of new parent', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set([assetStub.image.id])); + accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new'])); + when(assetMock.getById) .calledWith(assetStub.image.id) .mockResolvedValue(assetStub.image as AssetEntity); @@ -971,7 +1036,9 @@ describe(AssetService.name, () => { }); it('remove stackParentId of new parent', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set([assetStub.primaryImage.id])); + accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new'])); + await sut.updateStackParent(authStub.user1, { oldParentId: assetStub.primaryImage.id, newParentId: 'new', @@ -981,7 +1048,8 @@ describe(AssetService.name, () => { }); it('update stackParentId of old parents children to new parent', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set([assetStub.primaryImage.id])); + accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new'])); when(assetMock.getById) .calledWith(assetStub.primaryImage.id) .mockResolvedValue(assetStub.primaryImage as AssetEntity); @@ -997,4 +1065,18 @@ describe(AssetService.name, () => { ); }); }); + + it('get assets by device id', async () => { + const assets = [assetStub.image, assetStub.image1]; + + assetMock.getAllByDeviceId.mockImplementation(() => + Promise.resolve(Array.from(assets.map((asset) => asset.deviceAssetId))), + ); + + const deviceId = 'device-id'; + const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId); + + expect(result.length).toEqual(2); + expect(result).toEqual(assets.map((asset) => asset.deviceAssetId)); + }); }); diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index 34da78680..c547d6a6d 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -8,7 +8,7 @@ import { AccessCore, Permission } from '../access'; import { AuthUserDto } from '../auth'; import { mimeTypes } from '../domain.constant'; import { HumanReadableSize, usePagination } from '../domain.util'; -import { IAssetDeletionJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; +import { IAssetDeletionJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; import { CommunicationEvent, IAccessRepository, @@ -16,9 +16,11 @@ import { ICommunicationRepository, ICryptoRepository, IJobRepository, + IPartnerRepository, IStorageRepository, ISystemConfigRepository, ImmichReadStream, + TimeBucketOptions, } from '../repositories'; import { StorageCore, StorageFolder } from '../storage'; import { SystemConfigCore } from '../system-config'; @@ -28,6 +30,8 @@ import { AssetIdsDto, AssetJobName, AssetJobsDto, + AssetOrder, + AssetSearchDto, AssetStatsDto, DownloadArchiveInfo, DownloadInfoDto, @@ -83,11 +87,40 @@ export class AssetService { @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, + @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, ) { this.access = AccessCore.create(accessRepository); this.configCore = SystemConfigCore.create(configRepository); } + search(authUser: AuthUserDto, dto: AssetSearchDto) { + let checksum: Buffer | undefined = undefined; + + if (dto.checksum) { + const encoding = dto.checksum.length === 28 ? 'base64' : 'hex'; + checksum = Buffer.from(dto.checksum, encoding); + } + + const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const; + const order = dto.order ? enumToOrder[dto.order] : undefined; + + return this.assetRepository + .search({ + ...dto, + order, + checksum, + ownerId: authUser.id, + }) + .then((assets) => + assets.map((asset) => + mapAsset(asset, { + stripMetadata: false, + withStack: true, + }), + ), + ); + } + canUploadFile({ authUser, fieldName, file }: UploadRequest): true { this.access.requireUploadAccess(authUser); @@ -187,11 +220,25 @@ export class AssetService { await this.access.requirePermission(authUser, Permission.ARCHIVE_READ, [dto.userId]); } } + + if (dto.withPartners) { + const requestedArchived = dto.isArchived === true || dto.isArchived === undefined; + const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false; + const requestedTrash = dto.isTrashed === true; + + if (requestedArchived || requestedFavorite || requestedTrash) { + throw new BadRequestException( + 'withPartners is only supported for non-archived, non-trashed, non-favorited assets', + ); + } + } } async getTimeBuckets(authUser: AuthUserDto, dto: TimeBucketDto): Promise { await this.timeBucketChecks(authUser, dto); - return this.assetRepository.getTimeBuckets(dto); + const timeBucketOptions = await this.buildTimeBucketOptions(authUser, dto); + + return this.assetRepository.getTimeBuckets(timeBucketOptions); } async getTimeBucket( @@ -199,7 +246,8 @@ export class AssetService { dto: TimeBucketAssetDto, ): Promise { await this.timeBucketChecks(authUser, dto); - const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, dto); + const timeBucketOptions = await this.buildTimeBucketOptions(authUser, dto); + const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions); if (authUser.isShowMetadata) { return assets.map((asset) => mapAsset(asset, { withStack: true })); } else { @@ -207,6 +255,25 @@ export class AssetService { } } + async buildTimeBucketOptions(authUser: AuthUserDto, dto: TimeBucketDto): Promise { + const { userId, ...options } = dto; + let userIds: string[] | undefined = undefined; + + if (userId) { + userIds = [userId]; + + if (dto.withPartners) { + const partners = await this.partnerRepository.getAll(authUser.id); + const partnersIds = partners + .filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline) + .map((partner) => partner.sharedById); + + userIds.push(...partnersIds); + } + } + + return { ...options, userIds }; + } async downloadFile(authUser: AuthUserDto, id: string): Promise { await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, id); @@ -319,13 +386,15 @@ export class AssetService { return assets.map((a) => mapAsset(a)); } + async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) { + return this.assetRepository.getAllByDeviceId(authUser.id, deviceId); + } + async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise { await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id); - const { description, ...rest } = dto; - if (description !== undefined) { - await this.assetRepository.upsertExif({ assetId: id, description }); - } + const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto; + await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude }); const asset = await this.assetRepository.save({ id, ...rest }); await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids: [id] } }); @@ -333,7 +402,7 @@ export class AssetService { } async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise { - const { ids, removeParent, ...options } = dto; + const { ids, removeParent, dateTimeOriginal, latitude, longitude, ...options } = dto; await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids); if (removeParent) { @@ -353,6 +422,10 @@ export class AssetService { await this.assetRepository.updateAll([options.stackParentId], { stackParentId: null }); } + for (const id of ids) { + await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude }); + } + await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } }); await this.assetRepository.updateAll(ids, options); this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, authUser.id, ids); @@ -516,4 +589,13 @@ export class AssetService { } } } + + private async updateMetadata(dto: ISidecarWriteJob) { + const { id, description, dateTimeOriginal, latitude, longitude } = dto; + const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude }, _.isUndefined); + if (Object.keys(writes).length > 0) { + await this.assetRepository.upsertExif({ assetId: id, ...writes }); + await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } }); + } + } } diff --git a/server/src/domain/asset/dto/asset.dto.ts b/server/src/domain/asset/dto/asset.dto.ts index 0b3ce68d5..ac50f2242 100644 --- a/server/src/domain/asset/dto/asset.dto.ts +++ b/server/src/domain/asset/dto/asset.dto.ts @@ -1,8 +1,177 @@ +import { AssetType } from '@app/infra/entities'; +import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsBoolean, IsInt, IsPositive, IsString } from 'class-validator'; -import { Optional, ValidateUUID } from '../../domain.util'; +import { + IsBoolean, + IsDateString, + IsEnum, + IsInt, + IsLatitude, + IsLongitude, + IsNotEmpty, + IsPositive, + IsString, + Min, + ValidateIf, +} from 'class-validator'; +import { Optional, QueryBoolean, QueryDate, ValidateUUID } from '../../domain.util'; import { BulkIdsDto } from '../response-dto'; +export enum AssetOrder { + ASC = 'asc', + DESC = 'desc', +} + +const hasGPS = (o: { latitude: undefined; longitude: undefined }) => + o.latitude !== undefined || o.longitude !== undefined; +const ValidateGPS = () => ValidateIf(hasGPS); + +export class AssetSearchDto { + @ValidateUUID({ optional: true }) + id?: string; + + @ValidateUUID({ optional: true }) + libraryId?: string; + + @IsString() + @Optional() + deviceAssetId?: string; + + @IsString() + @Optional() + deviceId?: string; + + @IsEnum(AssetType) + @Optional() + @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) + type?: AssetType; + + @IsString() + @Optional() + checksum?: string; + + @QueryBoolean({ optional: true }) + isArchived?: boolean; + + @QueryBoolean({ optional: true }) + isEncoded?: boolean; + + @QueryBoolean({ optional: true }) + isExternal?: boolean; + + @QueryBoolean({ optional: true }) + isFavorite?: boolean; + + @QueryBoolean({ optional: true }) + isMotion?: boolean; + + @QueryBoolean({ optional: true }) + isOffline?: boolean; + + @QueryBoolean({ optional: true }) + isReadOnly?: boolean; + + @QueryBoolean({ optional: true }) + isVisible?: boolean; + + @QueryBoolean({ optional: true }) + withDeleted?: boolean; + + @QueryBoolean({ optional: true }) + withStacked?: boolean; + + @QueryBoolean({ optional: true }) + withExif?: boolean; + + @QueryBoolean({ optional: true }) + withPeople?: boolean; + + @QueryDate({ optional: true }) + createdBefore?: Date; + + @QueryDate({ optional: true }) + createdAfter?: Date; + + @QueryDate({ optional: true }) + updatedBefore?: Date; + + @QueryDate({ optional: true }) + updatedAfter?: Date; + + @QueryDate({ optional: true }) + trashedBefore?: Date; + + @QueryDate({ optional: true }) + trashedAfter?: Date; + + @QueryDate({ optional: true }) + takenBefore?: Date; + + @QueryDate({ optional: true }) + takenAfter?: Date; + + @IsString() + @Optional() + originalFileName?: string; + + @IsString() + @Optional() + originalPath?: string; + + @IsString() + @Optional() + resizePath?: string; + + @IsString() + @Optional() + webpPath?: string; + + @IsString() + @Optional() + encodedVideoPath?: string; + + @IsString() + @Optional() + city?: string; + + @IsString() + @Optional() + state?: string; + + @IsString() + @Optional() + country?: string; + + @IsString() + @Optional() + make?: string; + + @IsString() + @Optional() + model?: string; + + @IsString() + @Optional() + lensModel?: string; + + @IsEnum(AssetOrder) + @Optional() + @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) + order?: AssetOrder; + + @IsInt() + @Min(1) + @Type(() => Number) + @Optional() + page?: number; + + @IsInt() + @Min(1) + @Type(() => Number) + @Optional() + size?: number; +} + export class AssetBulkUpdateDto extends BulkIdsDto { @Optional() @IsBoolean() @@ -19,6 +188,20 @@ export class AssetBulkUpdateDto extends BulkIdsDto { @Optional() @IsBoolean() removeParent?: boolean; + + @Optional() + @IsDateString() + dateTimeOriginal?: string; + + @ValidateGPS() + @IsLatitude() + @IsNotEmpty() + latitude?: number; + + @ValidateGPS() + @IsLongitude() + @IsNotEmpty() + longitude?: number; } export class UpdateAssetDto { @@ -33,6 +216,20 @@ export class UpdateAssetDto { @Optional() @IsString() description?: string; + + @Optional() + @IsDateString() + dateTimeOriginal?: string; + + @ValidateGPS() + @IsLatitude() + @IsNotEmpty() + latitude?: number; + + @ValidateGPS() + @IsLongitude() + @IsNotEmpty() + longitude?: number; } export class RandomAssetsDto { diff --git a/server/src/domain/asset/dto/time-bucket.dto.ts b/server/src/domain/asset/dto/time-bucket.dto.ts index db2f1ecd0..849b8713f 100644 --- a/server/src/domain/asset/dto/time-bucket.dto.ts +++ b/server/src/domain/asset/dto/time-bucket.dto.ts @@ -38,6 +38,11 @@ export class TimeBucketDto { @IsBoolean() @Transform(toBoolean) withStacked?: boolean; + + @Optional() + @IsBoolean() + @Transform(toBoolean) + withPartners?: boolean; } export class TimeBucketAssetDto extends TimeBucketDto { diff --git a/server/src/domain/asset/response-dto/asset-response.dto.ts b/server/src/domain/asset/response-dto/asset-response.dto.ts index bacd4bfe6..72c257256 100644 --- a/server/src/domain/asset/response-dto/asset-response.dto.ts +++ b/server/src/domain/asset/response-dto/asset-response.dto.ts @@ -98,7 +98,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As tags: entity.tags?.map(mapTag), people: entity.faces ?.map(mapFace) - .filter((person): person is PersonResponseDto => person !== null && !person.isHidden) + .filter((person): person is PersonResponseDto => person !== null) .reduce((people, person) => { const existingPerson = people.find((p) => p.id === person.id); if (!existingPerson) { diff --git a/server/src/domain/auth/auth.dto.ts b/server/src/domain/auth/auth.dto.ts new file mode 100644 index 000000000..d5ee275ad --- /dev/null +++ b/server/src/domain/auth/auth.dto.ts @@ -0,0 +1,125 @@ +import { UserEntity, UserTokenEntity } from '@app/infra/entities'; +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; + +export class AuthUserDto { + id!: string; + email!: string; + isAdmin!: boolean; + isPublicUser?: boolean; + sharedLinkId?: string; + isAllowUpload?: boolean; + isAllowDownload?: boolean; + isShowMetadata?: boolean; + accessTokenId?: string; + externalPath?: string | null; +} + +export class LoginCredentialDto { + @IsEmail({ require_tld: false }) + @Transform(({ value }) => value?.toLowerCase()) + @IsNotEmpty() + @ApiProperty({ example: 'testuser@email.com' }) + email!: string; + + @IsString() + @IsNotEmpty() + @ApiProperty({ example: 'password' }) + password!: string; +} + +export class LoginResponseDto { + accessToken!: string; + userId!: string; + userEmail!: string; + name!: string; + profileImagePath!: string; + isAdmin!: boolean; + shouldChangePassword!: boolean; +} + +export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto { + return { + accessToken: accessToken, + userId: entity.id, + userEmail: entity.email, + name: entity.name, + isAdmin: entity.isAdmin, + profileImagePath: entity.profileImagePath, + shouldChangePassword: entity.shouldChangePassword, + }; +} + +export class LogoutResponseDto { + successful!: boolean; + redirectUri!: string; +} + +export class SignUpDto extends LoginCredentialDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ example: 'Admin' }) + name!: string; +} + +export class ChangePasswordDto { + @IsString() + @IsNotEmpty() + @ApiProperty({ example: 'password' }) + password!: string; + + @IsString() + @IsNotEmpty() + @MinLength(8) + @ApiProperty({ example: 'password' }) + newPassword!: string; +} + +export class ValidateAccessTokenResponseDto { + authStatus!: boolean; +} + +export class AuthDeviceResponseDto { + id!: string; + createdAt!: string; + updatedAt!: string; + current!: boolean; + deviceType!: string; + deviceOS!: string; +} + +export const mapUserToken = (entity: UserTokenEntity, currentId?: string): AuthDeviceResponseDto => ({ + id: entity.id, + createdAt: entity.createdAt.toISOString(), + updatedAt: entity.updatedAt.toISOString(), + current: currentId === entity.id, + deviceOS: entity.deviceOS, + deviceType: entity.deviceType, +}); + +export class OAuthCallbackDto { + @IsNotEmpty() + @IsString() + @ApiProperty() + url!: string; +} + +export class OAuthConfigDto { + @IsNotEmpty() + @IsString() + redirectUri!: string; +} + +/** @deprecated use oauth authorize */ +export class OAuthConfigResponseDto { + enabled!: boolean; + passwordLoginEnabled!: boolean; + url?: string; + buttonText?: string; + autoLaunch?: boolean; +} + +export class OAuthAuthorizeResponseDto { + url!: string; +} diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/domain/auth/auth.service.spec.ts index 2ef598b05..7ece7bed8 100644 --- a/server/src/domain/auth/auth.service.spec.ts +++ b/server/src/domain/auth/auth.service.spec.ts @@ -31,8 +31,8 @@ import { IUserTokenRepository, } from '../repositories'; import { AuthType } from './auth.constant'; +import { AuthUserDto, SignUpDto } from './auth.dto'; import { AuthService } from './auth.service'; -import { AuthUserDto, SignUpDto } from './dto'; // const token = Buffer.from('my-api-key', 'utf8').toString('base64'); @@ -236,7 +236,7 @@ describe('AuthService', () => { }); describe('adminSignUp', () => { - const dto: SignUpDto = { email: 'test@immich.com', password: 'password', firstName: 'immich', lastName: 'admin' }; + const dto: SignUpDto = { email: 'test@immich.com', password: 'password', name: 'immich admin' }; it('should only allow one admin', async () => { userMock.getAdmin.mockResolvedValue({} as UserEntity); @@ -248,11 +248,11 @@ describe('AuthService', () => { userMock.getAdmin.mockResolvedValue(null); userMock.create.mockResolvedValue({ ...dto, id: 'admin', createdAt: new Date('2021-01-01') } as UserEntity); await expect(sut.adminSignUp(dto)).resolves.toEqual({ + avatarColor: expect.any(String), id: 'admin', createdAt: new Date('2021-01-01'), email: 'test@immich.com', - firstName: 'immich', - lastName: 'admin', + name: 'immich admin', }); expect(userMock.getAdmin).toHaveBeenCalled(); expect(userMock.create).toHaveBeenCalled(); @@ -395,11 +395,11 @@ describe('AuthService', () => { describe('logoutDevice', () => { it('should logout the device', async () => { - accessMock.authDevice.hasOwnerAccess.mockResolvedValue(true); + accessMock.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1'])); await sut.logoutDevice(authStub.user1, 'token-1'); - expect(accessMock.authDevice.hasOwnerAccess).toHaveBeenCalledWith(authStub.user1.id, 'token-1'); + expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.id, new Set(['token-1'])); expect(userTokenMock.delete).toHaveBeenCalledWith('token-1'); }); }); diff --git a/server/src/domain/auth/auth.service.ts b/server/src/domain/auth/auth.service.ts index 289f50bfb..7fa3d6902 100644 --- a/server/src/domain/auth/auth.service.ts +++ b/server/src/domain/auth/auth.service.ts @@ -32,18 +32,21 @@ import { LOGIN_URL, MOBILE_REDIRECT, } from './auth.constant'; -import { AuthUserDto, ChangePasswordDto, LoginCredentialDto, OAuthCallbackDto, OAuthConfigDto, SignUpDto } from './dto'; import { - AdminSignupResponseDto, AuthDeviceResponseDto, + AuthUserDto, + ChangePasswordDto, + LoginCredentialDto, LoginResponseDto, LogoutResponseDto, OAuthAuthorizeResponseDto, + OAuthCallbackDto, + OAuthConfigDto, OAuthConfigResponseDto, - mapAdminSignupResponse, + SignUpDto, mapLoginResponse, mapUserToken, -} from './response-dto'; +} from './auth.dto'; export interface LoginDetails { isSecure: boolean; @@ -133,7 +136,7 @@ export class AuthService { return this.userCore.updateUser(authUser, authUser.id, { password: newPassword }); } - async adminSignUp(dto: SignUpDto): Promise { + async adminSignUp(dto: SignUpDto): Promise { const adminUser = await this.userRepository.getAdmin(); if (adminUser) { @@ -143,13 +146,12 @@ export class AuthService { const admin = await this.userCore.createUser({ isAdmin: true, email: dto.email, - firstName: dto.firstName, - lastName: dto.lastName, + name: dto.name, password: dto.password, storageLabel: 'admin', }); - return mapAdminSignupResponse(admin); + return mapUser(admin); } async validate(headers: IncomingHttpHeaders, params: Record): Promise { @@ -270,9 +272,9 @@ export class AuthService { storageLabel = null; } + const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`; user = await this.userCore.createUser({ - firstName: profile.given_name || '', - lastName: profile.family_name || '', + name: userName, email: profile.email, oauthId: profile.sub, storageLabel, diff --git a/server/src/domain/auth/dto/auth-user.dto.ts b/server/src/domain/auth/dto/auth-user.dto.ts deleted file mode 100644 index a689096d8..000000000 --- a/server/src/domain/auth/dto/auth-user.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class AuthUserDto { - id!: string; - email!: string; - isAdmin!: boolean; - isPublicUser?: boolean; - sharedLinkId?: string; - isAllowUpload?: boolean; - isAllowDownload?: boolean; - isShowMetadata?: boolean; - accessTokenId?: string; - externalPath?: string | null; -} diff --git a/server/src/domain/auth/dto/change-password.dto.ts b/server/src/domain/auth/dto/change-password.dto.ts deleted file mode 100644 index 9c5ce479e..000000000 --- a/server/src/domain/auth/dto/change-password.dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, MinLength } from 'class-validator'; - -export class ChangePasswordDto { - @IsString() - @IsNotEmpty() - @ApiProperty({ example: 'password' }) - password!: string; - - @IsString() - @IsNotEmpty() - @MinLength(8) - @ApiProperty({ example: 'password' }) - newPassword!: string; -} diff --git a/server/src/domain/auth/dto/index.ts b/server/src/domain/auth/dto/index.ts deleted file mode 100644 index 59a65770a..000000000 --- a/server/src/domain/auth/dto/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './auth-user.dto'; -export * from './change-password.dto'; -export * from './login-credential.dto'; -export * from './oauth-auth-code.dto'; -export * from './oauth-config.dto'; -export * from './sign-up.dto'; diff --git a/server/src/domain/auth/dto/login-credential.dto.spec.ts b/server/src/domain/auth/dto/login-credential.dto.spec.ts deleted file mode 100644 index 7682db4c4..000000000 --- a/server/src/domain/auth/dto/login-credential.dto.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { plainToInstance } from 'class-transformer'; -import { validateSync } from 'class-validator'; -import { LoginCredentialDto } from './login-credential.dto'; - -describe('LoginCredentialDto', () => { - it('should allow emails without a tld', () => { - const someEmail = 'test@test'; - - const dto = plainToInstance(LoginCredentialDto, { email: someEmail, password: 'password' }); - const errors = validateSync(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual(someEmail); - }); - - it('should fail without an email', () => { - const dto = plainToInstance(LoginCredentialDto, { password: 'password' }); - const errors = validateSync(dto); - expect(errors).toHaveLength(1); - expect(errors[0].property).toEqual('email'); - }); - - it('should fail with an invalid email', () => { - const dto = plainToInstance(LoginCredentialDto, { email: 'invalid.com', password: 'password' }); - const errors = validateSync(dto); - expect(errors).toHaveLength(1); - expect(errors[0].property).toEqual('email'); - }); - - it('should make the email all lowercase', () => { - const dto = plainToInstance(LoginCredentialDto, { email: 'TeSt@ImMiCh.com', password: 'password' }); - const errors = validateSync(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual('test@immich.com'); - }); - - it('should fail without a password', () => { - const dto = plainToInstance(LoginCredentialDto, { email: 'test@immich.com', password: '' }); - const errors = validateSync(dto); - expect(errors).toHaveLength(1); - expect(errors[0].property).toEqual('password'); - }); -}); diff --git a/server/src/domain/auth/dto/login-credential.dto.ts b/server/src/domain/auth/dto/login-credential.dto.ts deleted file mode 100644 index 12516a108..000000000 --- a/server/src/domain/auth/dto/login-credential.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; - -export class LoginCredentialDto { - @IsEmail({ require_tld: false }) - @Transform(({ value }) => value?.toLowerCase()) - @IsNotEmpty() - @ApiProperty({ example: 'testuser@email.com' }) - email!: string; - - @IsString() - @IsNotEmpty() - @ApiProperty({ example: 'password' }) - password!: string; -} diff --git a/server/src/domain/auth/dto/oauth-auth-code.dto.ts b/server/src/domain/auth/dto/oauth-auth-code.dto.ts deleted file mode 100644 index 924db0052..000000000 --- a/server/src/domain/auth/dto/oauth-auth-code.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class OAuthCallbackDto { - @IsNotEmpty() - @IsString() - @ApiProperty() - url!: string; -} diff --git a/server/src/domain/auth/dto/oauth-config.dto.ts b/server/src/domain/auth/dto/oauth-config.dto.ts deleted file mode 100644 index a14fc19dc..000000000 --- a/server/src/domain/auth/dto/oauth-config.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IsNotEmpty, IsString } from 'class-validator'; - -export class OAuthConfigDto { - @IsNotEmpty() - @IsString() - redirectUri!: string; -} diff --git a/server/src/domain/auth/dto/sign-up.dto.spec.ts b/server/src/domain/auth/dto/sign-up.dto.spec.ts deleted file mode 100644 index de708fe2e..000000000 --- a/server/src/domain/auth/dto/sign-up.dto.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { plainToInstance } from 'class-transformer'; -import { validateSync } from 'class-validator'; -import { SignUpDto } from './sign-up.dto'; - -describe('SignUpDto', () => { - it('should require all fields', () => { - const dto = plainToInstance(SignUpDto, { - email: '', - password: '', - firstName: '', - lastName: '', - }); - const errors = validateSync(dto); - expect(errors).toHaveLength(4); - expect(errors[0].property).toEqual('email'); - expect(errors[1].property).toEqual('password'); - expect(errors[2].property).toEqual('firstName'); - expect(errors[3].property).toEqual('lastName'); - }); - - it('should require a valid email', () => { - const dto = plainToInstance(SignUpDto, { - email: 'immich.com', - password: 'password', - firstName: 'first name', - lastName: 'last name', - }); - const errors = validateSync(dto); - expect(errors).toHaveLength(1); - expect(errors[0].property).toEqual('email'); - }); - - it('should allow emails without a tld', () => { - const someEmail = 'test@test'; - - const dto = plainToInstance(SignUpDto, { - email: someEmail, - password: 'password', - firstName: 'first name', - lastName: 'last name', - }); - const errors = validateSync(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual(someEmail); - }); - - it('should make the email all lowercase', () => { - const dto = plainToInstance(SignUpDto, { - email: 'TeSt@ImMiCh.com', - password: 'password', - firstName: 'first name', - lastName: 'last name', - }); - const errors = validateSync(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual('test@immich.com'); - }); -}); diff --git a/server/src/domain/auth/dto/sign-up.dto.ts b/server/src/domain/auth/dto/sign-up.dto.ts deleted file mode 100644 index 66741eb7e..000000000 --- a/server/src/domain/auth/dto/sign-up.dto.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; - -export class SignUpDto { - @IsEmail({ require_tld: false }) - @Transform(({ value }) => value?.toLowerCase()) - @IsNotEmpty() - @ApiProperty({ example: 'testuser@email.com' }) - email!: string; - - @IsString() - @IsNotEmpty() - @ApiProperty({ example: 'password' }) - password!: string; - - @IsString() - @IsNotEmpty() - @ApiProperty({ example: 'Admin' }) - firstName!: string; - - @IsString() - @IsNotEmpty() - @ApiProperty({ example: 'Doe' }) - lastName!: string; -} diff --git a/server/src/domain/auth/index.ts b/server/src/domain/auth/index.ts index d3aa704ba..52e0463bc 100644 --- a/server/src/domain/auth/index.ts +++ b/server/src/domain/auth/index.ts @@ -1,4 +1,3 @@ export * from './auth.constant'; +export * from './auth.dto'; export * from './auth.service'; -export * from './dto'; -export * from './response-dto'; diff --git a/server/src/domain/auth/response-dto/admin-signup-response.dto.ts b/server/src/domain/auth/response-dto/admin-signup-response.dto.ts deleted file mode 100644 index 5c2e4413c..000000000 --- a/server/src/domain/auth/response-dto/admin-signup-response.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { UserEntity } from '@app/infra/entities'; - -export class AdminSignupResponseDto { - id!: string; - email!: string; - firstName!: string; - lastName!: string; - createdAt!: Date; -} - -export function mapAdminSignupResponse(entity: UserEntity): AdminSignupResponseDto { - return { - id: entity.id, - email: entity.email, - firstName: entity.firstName, - lastName: entity.lastName, - createdAt: entity.createdAt, - }; -} diff --git a/server/src/domain/auth/response-dto/auth-device-response.dto.ts b/server/src/domain/auth/response-dto/auth-device-response.dto.ts deleted file mode 100644 index 986f743c0..000000000 --- a/server/src/domain/auth/response-dto/auth-device-response.dto.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { UserTokenEntity } from '@app/infra/entities'; - -export class AuthDeviceResponseDto { - id!: string; - createdAt!: string; - updatedAt!: string; - current!: boolean; - deviceType!: string; - deviceOS!: string; -} - -export const mapUserToken = (entity: UserTokenEntity, currentId?: string): AuthDeviceResponseDto => ({ - id: entity.id, - createdAt: entity.createdAt.toISOString(), - updatedAt: entity.updatedAt.toISOString(), - current: currentId === entity.id, - deviceOS: entity.deviceOS, - deviceType: entity.deviceType, -}); diff --git a/server/src/domain/auth/response-dto/index.ts b/server/src/domain/auth/response-dto/index.ts deleted file mode 100644 index 491c957fd..000000000 --- a/server/src/domain/auth/response-dto/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './admin-signup-response.dto'; -export * from './auth-device-response.dto'; -export * from './login-response.dto'; -export * from './logout-response.dto'; -export * from './oauth-config-response.dto'; -export * from './validate-asset-token-response.dto'; diff --git a/server/src/domain/auth/response-dto/login-response.dto.ts b/server/src/domain/auth/response-dto/login-response.dto.ts deleted file mode 100644 index 1be804996..000000000 --- a/server/src/domain/auth/response-dto/login-response.dto.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { UserEntity } from '@app/infra/entities'; -import { ApiResponseProperty } from '@nestjs/swagger'; - -export class LoginResponseDto { - @ApiResponseProperty() - accessToken!: string; - - @ApiResponseProperty() - userId!: string; - - @ApiResponseProperty() - userEmail!: string; - - @ApiResponseProperty() - firstName!: string; - - @ApiResponseProperty() - lastName!: string; - - @ApiResponseProperty() - profileImagePath!: string; - - @ApiResponseProperty() - isAdmin!: boolean; - - @ApiResponseProperty() - shouldChangePassword!: boolean; -} - -export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto { - return { - accessToken: accessToken, - userId: entity.id, - userEmail: entity.email, - firstName: entity.firstName, - lastName: entity.lastName, - isAdmin: entity.isAdmin, - profileImagePath: entity.profileImagePath, - shouldChangePassword: entity.shouldChangePassword, - }; -} diff --git a/server/src/domain/auth/response-dto/logout-response.dto.ts b/server/src/domain/auth/response-dto/logout-response.dto.ts deleted file mode 100644 index 16816264e..000000000 --- a/server/src/domain/auth/response-dto/logout-response.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class LogoutResponseDto { - successful!: boolean; - redirectUri!: string; -} diff --git a/server/src/domain/auth/response-dto/oauth-config-response.dto.ts b/server/src/domain/auth/response-dto/oauth-config-response.dto.ts deleted file mode 100644 index dadd66f59..000000000 --- a/server/src/domain/auth/response-dto/oauth-config-response.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -export class OAuthConfigResponseDto { - enabled!: boolean; - passwordLoginEnabled!: boolean; - url?: string; - buttonText?: string; - autoLaunch?: boolean; -} - -export class OAuthAuthorizeResponseDto { - url!: string; -} diff --git a/server/src/domain/auth/response-dto/validate-asset-token-response.dto.ts b/server/src/domain/auth/response-dto/validate-asset-token-response.dto.ts deleted file mode 100644 index 4fdb2971d..000000000 --- a/server/src/domain/auth/response-dto/validate-asset-token-response.dto.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class ValidateAccessTokenResponseDto { - authStatus!: boolean; -} diff --git a/server/src/domain/domain.constant.spec.ts b/server/src/domain/domain.constant.spec.ts index 0db2b51bf..016325430 100644 --- a/server/src/domain/domain.constant.spec.ts +++ b/server/src/domain/domain.constant.spec.ts @@ -7,6 +7,7 @@ describe('mimeTypes', () => { { mimetype: 'image/ari', extension: '.ari' }, { mimetype: 'image/arw', extension: '.arw' }, { mimetype: 'image/avif', extension: '.avif' }, + { mimetype: 'image/bmp', extension: '.bmp' }, { mimetype: 'image/cap', extension: '.cap' }, { mimetype: 'image/cin', extension: '.cin' }, { mimetype: 'image/cr2', extension: '.cr2' }, diff --git a/server/src/domain/domain.constant.ts b/server/src/domain/domain.constant.ts index 1b301be56..a4c17ff2d 100644 --- a/server/src/domain/domain.constant.ts +++ b/server/src/domain/domain.constant.ts @@ -63,6 +63,7 @@ const image: Record = { '.ari': ['image/ari', 'image/x-arriflex-ari'], '.arw': ['image/arw', 'image/x-sony-arw'], '.avif': ['image/avif'], + '.bmp': ['image/bmp'], '.cap': ['image/cap', 'image/x-phaseone-cap'], '.cin': ['image/cin', 'image/x-phantom-cin'], '.cr2': ['image/cr2', 'image/x-canon-cr2'], diff --git a/server/src/domain/domain.util.ts b/server/src/domain/domain.util.ts index 04ec4f430..1b1c0a3e2 100644 --- a/server/src/domain/domain.util.ts +++ b/server/src/domain/domain.util.ts @@ -1,6 +1,17 @@ import { applyDecorators } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsNotEmpty, IsOptional, IsString, IsUUID, ValidateIf, ValidationOptions } from 'class-validator'; +import { Transform, Type } from 'class-transformer'; +import { + IsArray, + IsBoolean, + IsDate, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, + ValidateIf, + ValidationOptions, +} from 'class-validator'; import { CronJob } from 'cron'; import { basename, extname } from 'node:path'; import sanitize from 'sanitize-filename'; @@ -33,6 +44,22 @@ interface IValue { value?: string; } +export const QueryBoolean = ({ optional }: { optional?: boolean }) => { + const decorators = [IsBoolean(), Transform(toBoolean)]; + if (optional) { + decorators.push(Optional()); + } + return applyDecorators(...decorators); +}; + +export const QueryDate = ({ optional }: { optional?: boolean }) => { + const decorators = [IsDate(), Type(() => Date)]; + if (optional) { + decorators.push(Optional()); + } + return applyDecorators(...decorators); +}; + export const toBoolean = ({ value }: IValue) => { if (value == 'true') { return true; @@ -123,3 +150,40 @@ export function Optional({ nullable, ...validationOptions }: OptionalOptions = { return ValidateIf((obj: any, v: any) => v !== undefined, validationOptions); } + +// NOTE: The following Set utils have been added here, to easily determine where they are used. +// They should be replaced with native Set operations, when they are added to the language. +// Proposal reference: https://github.com/tc39/proposal-set-methods + +export const setUnion = (...sets: Set[]): Set => { + const union = new Set(sets[0]); + for (const set of sets.slice(1)) { + for (const elem of set) { + union.add(elem); + } + } + return union; +}; + +export const setDifference = (setA: Set, ...sets: Set[]): Set => { + const difference = new Set(setA); + for (const set of sets) { + for (const elem of set) { + difference.delete(elem); + } + } + return difference; +}; + +export const setIsSuperset = (set: Set, subset: Set): boolean => { + for (const elem of subset) { + if (!set.has(elem)) { + return false; + } + } + return true; +}; + +export const setIsEqual = (setA: Set, setB: Set): boolean => { + return setA.size === setB.size && setIsSuperset(setA, setB); +}; diff --git a/server/src/domain/job/job.constants.ts b/server/src/domain/job/job.constants.ts index c5b4fe235..a7f467784 100644 --- a/server/src/domain/job/job.constants.ts +++ b/server/src/domain/job/job.constants.ts @@ -96,6 +96,7 @@ export enum JobName { QUEUE_SIDECAR = 'queue-sidecar', SIDECAR_DISCOVERY = 'sidecar-discovery', SIDECAR_SYNC = 'sidecar-sync', + SIDECAR_WRITE = 'sidecar-write', } export const JOBS_ASSET_PAGINATION_SIZE = 1000; @@ -168,6 +169,7 @@ export const JOBS_TO_QUEUE: Record = { [JobName.QUEUE_SIDECAR]: QueueName.SIDECAR, [JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR, [JobName.SIDECAR_SYNC]: QueueName.SIDECAR, + [JobName.SIDECAR_WRITE]: QueueName.SIDECAR, // Library management [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY, diff --git a/server/src/domain/job/job.interface.ts b/server/src/domain/job/job.interface.ts index 033dfdac4..be76f6645 100644 --- a/server/src/domain/job/job.interface.ts +++ b/server/src/domain/job/job.interface.ts @@ -9,7 +9,7 @@ export interface IAssetFaceJob extends IBaseJob { export interface IEntityJob extends IBaseJob { id: string; - source?: 'upload'; + source?: 'upload' | 'sidecar-write'; } export interface IAssetDeletionJob extends IEntityJob { @@ -33,3 +33,10 @@ export interface IBulkEntityJob extends IBaseJob { export interface IDeleteFilesJob extends IBaseJob { files: Array; } + +export interface ISidecarWriteJob extends IEntityJob { + description?: string; + dateTimeOriginal?: string; + latitude?: number; + longitude?: number; +} diff --git a/server/src/domain/job/job.service.ts b/server/src/domain/job/job.service.ts index 7ebffcc69..4735eb6b5 100644 --- a/server/src/domain/job/job.service.ts +++ b/server/src/domain/job/job.service.ts @@ -165,7 +165,19 @@ export class JobService { await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: item.data }); break; + case JobName.SIDECAR_WRITE: + await this.jobRepository.queue({ + name: JobName.METADATA_EXTRACTION, + data: { id: item.data.id, source: 'sidecar-write' }, + }); + case JobName.METADATA_EXTRACTION: + if (item.data.source === 'sidecar-write') { + const [asset] = await this.assetRepository.getByIds([item.data.id]); + if (asset) { + this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, asset.ownerId, mapAsset(asset)); + } + } await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data }); break; diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index 3d7d68736..f49600a36 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -58,7 +58,8 @@ describe(LibraryService.name, () => { ctime: new Date('2023-01-01'), } as Stats); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + // Always validate owner access for library. + accessMock.library.checkOwnerAccess.mockImplementation(async (_, libraryIds) => libraryIds); sut = new LibraryService( accessMock, diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/domain/metadata/metadata.service.spec.ts index f31605b12..0ef5dfd73 100644 --- a/server/src/domain/metadata/metadata.service.spec.ts +++ b/server/src/domain/metadata/metadata.service.spec.ts @@ -1,4 +1,4 @@ -import { AssetType, CitiesFile, ExifEntity, SystemConfigKey } from '@app/infra/entities'; +import { AssetType, ExifEntity, SystemConfigKey } from '@app/infra/entities'; import { assetStub, newAlbumRepositoryMock, @@ -14,7 +14,8 @@ import { import { randomBytes } from 'crypto'; import { Stats } from 'fs'; import { constants } from 'fs/promises'; -import { JobName, QueueName } from '../job'; +import { when } from 'jest-when'; +import { JobName } from '../job'; import { IAlbumRepository, IAssetRepository, @@ -77,10 +78,7 @@ describe(MetadataService.name, () => { describe('init', () => { beforeEach(async () => { - configMock.load.mockResolvedValue([ - { key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true }, - { key: SystemConfigKey.REVERSE_GEOCODING_CITIES_FILE_OVERRIDE, value: CitiesFile.CITIES_500 }, - ]); + configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true }]); await sut.init(); }); @@ -89,42 +87,10 @@ describe(MetadataService.name, () => { configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: false }]); await sut.init(); - expect(metadataMock.deleteCache).not.toHaveBeenCalled(); expect(jobMock.pause).toHaveBeenCalledTimes(1); expect(metadataMock.init).toHaveBeenCalledTimes(1); expect(jobMock.resume).toHaveBeenCalledTimes(1); }); - - it('should return if deleteCache is false and the cities precision has not changed', async () => { - await sut.init(); - - expect(metadataMock.deleteCache).not.toHaveBeenCalled(); - expect(jobMock.pause).toHaveBeenCalledTimes(1); - expect(metadataMock.init).toHaveBeenCalledTimes(1); - expect(jobMock.resume).toHaveBeenCalledTimes(1); - }); - - it('should re-init if deleteCache is false but the cities precision has changed', async () => { - configMock.load.mockResolvedValue([ - { key: SystemConfigKey.REVERSE_GEOCODING_CITIES_FILE_OVERRIDE, value: CitiesFile.CITIES_1000 }, - ]); - - await sut.init(); - - expect(metadataMock.deleteCache).not.toHaveBeenCalled(); - expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); - expect(metadataMock.init).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_1000 }); - expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); - }); - - it('should re-init and delete cache if deleteCache is true', async () => { - await sut.init(true); - - expect(metadataMock.deleteCache).toHaveBeenCalled(); - expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); - expect(metadataMock.init).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_500 }); - expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); - }); }); describe('handleLivePhotoLinking', () => { @@ -248,9 +214,33 @@ describe(MetadataService.name, () => { expect(assetMock.save).not.toHaveBeenCalled(); }); + it('should handle a date in a sidecar file', async () => { + const originalDate = new Date('2023-11-21T16:13:17.517Z'); + const sidecarDate = new Date('2022-01-01T00:00:00.000Z'); + assetMock.getByIds.mockResolvedValue([assetStub.sidecar]); + when(metadataMock.readTags) + .calledWith(assetStub.sidecar.originalPath) + // higher priority tag + .mockResolvedValue({ CreationDate: originalDate.toISOString() }); + when(metadataMock.readTags) + .calledWith(assetStub.sidecar.sidecarPath as string) + // lower priority tag, but in sidecar + .mockResolvedValue({ CreateDate: sidecarDate.toISOString() }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.sidecar.id]); + expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ dateTimeOriginal: sidecarDate })); + expect(assetMock.save).toHaveBeenCalledWith({ + id: assetStub.image.id, + duration: null, + fileCreatedAt: sidecarDate, + localDateTime: sidecarDate, + }); + }); + it('should handle lists of numbers', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.getExifTags.mockResolvedValue({ ISO: [160] as any }); + metadataMock.readTags.mockResolvedValue({ ISO: [160] as any }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); @@ -267,7 +257,7 @@ describe(MetadataService.name, () => { assetMock.getByIds.mockResolvedValue([assetStub.withLocation]); configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true }]); metadataMock.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' }); - metadataMock.getExifTags.mockResolvedValue({ + metadataMock.readTags.mockResolvedValue({ GPSLatitude: assetStub.withLocation.exifInfo!.latitude!, GPSLongitude: assetStub.withLocation.exifInfo!.longitude!, }); @@ -299,7 +289,7 @@ describe(MetadataService.name, () => { it('should apply motion photos', async () => { assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]); - metadataMock.getExifTags.mockResolvedValue({ + metadataMock.readTags.mockResolvedValue({ Directory: 'foo/bar/', MotionPhoto: 1, MicroVideo: 1, @@ -320,7 +310,7 @@ describe(MetadataService.name, () => { it('should create new motion asset if not found and link it with the photo', async () => { assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoStillAsset, livePhotoVideoId: null }]); - metadataMock.getExifTags.mockResolvedValue({ + metadataMock.readTags.mockResolvedValue({ Directory: 'foo/bar/', MotionPhoto: 1, MicroVideo: 1, @@ -377,7 +367,7 @@ describe(MetadataService.name, () => { tz: '+02:00', }; assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.getExifTags.mockResolvedValue(tags); + metadataMock.readTags.mockResolvedValue(tags); await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); @@ -416,7 +406,7 @@ describe(MetadataService.name, () => { it('should handle duration', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.getExifTags.mockResolvedValue({ Duration: 6.21 }); + metadataMock.readTags.mockResolvedValue({ Duration: 6.21 }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -432,7 +422,7 @@ describe(MetadataService.name, () => { it('should handle duration as an object without Scale', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.getExifTags.mockResolvedValue({ Duration: { Value: 6.2 } }); + metadataMock.readTags.mockResolvedValue({ Duration: { Value: 6.2 } }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -448,7 +438,7 @@ describe(MetadataService.name, () => { it('should handle duration with scale', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.getExifTags.mockResolvedValue({ Duration: { Scale: 1.11111111111111e-5, Value: 558720 } }); + metadataMock.readTags.mockResolvedValue({ Duration: { Scale: 1.11111111111111e-5, Value: 558720 } }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -541,4 +531,41 @@ describe(MetadataService.name, () => { }); }); }); + + describe('handleSidecarWrite', () => { + it('should skip assets that do not exist anymore', async () => { + assetMock.getByIds.mockResolvedValue([]); + await expect(sut.handleSidecarWrite({ id: 'asset-123' })).resolves.toBe(false); + expect(metadataMock.writeTags).not.toHaveBeenCalled(); + }); + + it('should skip jobs with not metadata', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.sidecar]); + await expect(sut.handleSidecarWrite({ id: assetStub.sidecar.id })).resolves.toBe(true); + expect(metadataMock.writeTags).not.toHaveBeenCalled(); + }); + + it('should write tags', async () => { + const description = 'this is a description'; + const gps = 12; + const date = '2023-11-22T04:56:12.196Z'; + + assetMock.getByIds.mockResolvedValue([assetStub.sidecar]); + await expect( + sut.handleSidecarWrite({ + id: assetStub.sidecar.id, + description, + latitude: gps, + longitude: gps, + dateTimeOriginal: date, + }), + ).resolves.toBe(true); + expect(metadataMock.writeTags).toHaveBeenCalledWith(assetStub.sidecar.sidecarPath, { + ImageDescription: description, + CreationDate: date, + GPSLatitude: gps, + GPSLongitude: gps, + }); + }); + }); }); diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 45193c2e1..e9c7ff931 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -3,10 +3,11 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { ExifDateTime, Tags } from 'exiftool-vendored'; import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime'; import { constants } from 'fs/promises'; +import _ from 'lodash'; import { Duration } from 'luxon'; import { Subscription } from 'rxjs'; import { usePagination } from '../domain.util'; -import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job'; +import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job'; import { ExifDuration, IAlbumRepository, @@ -25,6 +26,18 @@ import { import { StorageCore } from '../storage'; import { FeatureFlag, SystemConfigCore } from '../system-config'; +/** look for a date from these tags (in order) */ +const EXIF_DATE_TAGS: Array = [ + 'SubSecDateTimeOriginal', + 'DateTimeOriginal', + 'SubSecCreateDate', + 'CreationDate', + 'CreateDate', + 'SubSecMediaCreateDate', + 'MediaCreateDate', + 'DateTimeCreated', +]; + interface DirectoryItem { Length?: number; Mime: string; @@ -39,7 +52,7 @@ interface DirectoryEntry { type ExifEntityWithoutGeocodeAndTypeOrm = Omit< ExifEntity, 'city' | 'state' | 'country' | 'description' | 'exifTextSearchableColumn' ->; +> & { dateTimeOriginal: Date }; const exifDate = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.toDate() : null); const tzOffset = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.tzoffsetMinutes : null); @@ -67,7 +80,6 @@ export class MetadataService { private logger = new Logger(MetadataService.name); private storageCore: StorageCore; private configCore: SystemConfigCore; - private oldCities?: string; private subscription: Subscription | null = null; constructor( @@ -85,31 +97,24 @@ export class MetadataService { this.storageCore = StorageCore.create(assetRepository, moveRepository, personRepository, storageRepository); } - async init(deleteCache = false) { + async init() { if (!this.subscription) { this.subscription = this.configCore.config$.subscribe(() => this.init()); } const { reverseGeocoding } = await this.configCore.getConfig(); - const { citiesFileOverride } = reverseGeocoding; + const { enabled } = reverseGeocoding; - if (!reverseGeocoding.enabled) { + if (!enabled) { return; } try { - if (deleteCache) { - await this.repository.deleteCache(); - } else if (this.oldCities && this.oldCities === citiesFileOverride) { - return; - } - await this.jobRepository.pause(QueueName.METADATA_EXTRACTION); - await this.repository.init({ citiesFileOverride }); + await this.repository.init(); await this.jobRepository.resume(QueueName.METADATA_EXTRACTION); - this.logger.log(`Initialized local reverse geocoder with ${citiesFileOverride}`); - this.oldCities = citiesFileOverride; + this.logger.log(`Initialized local reverse geocoder`); } catch (error: Error | any) { this.logger.error(`Unable to initialize reverse geocoding: ${error}`, error?.stack); } @@ -181,7 +186,7 @@ export class MetadataService { await this.applyReverseGeocoding(asset, exifData); await this.assetRepository.upsertExif(exifData); - const dateTimeOriginal = exifDate(firstDateTime(tags as Tags)) ?? exifData.dateTimeOriginal; + const dateTimeOriginal = exifData.dateTimeOriginal; let localDateTime = dateTimeOriginal ?? undefined; const timeZoneOffset = tzOffset(firstDateTime(tags as Tags)) ?? 0; @@ -239,6 +244,37 @@ export class MetadataService { return true; } + async handleSidecarWrite(job: ISidecarWriteJob) { + const { id, description, dateTimeOriginal, latitude, longitude } = job; + const [asset] = await this.assetRepository.getByIds([id]); + if (!asset) { + return false; + } + + const sidecarPath = asset.sidecarPath || `${asset.originalPath}.xmp`; + const exif = _.omitBy( + { + ImageDescription: description, + CreationDate: dateTimeOriginal, + GPSLatitude: latitude, + GPSLongitude: longitude, + }, + _.isUndefined, + ); + + if (Object.keys(exif).length === 0) { + return true; + } + + await this.repository.writeTags(sidecarPath, exif); + + if (!asset.sidecarPath) { + await this.assetRepository.save({ id, sidecarPath }); + } + + return true; + } + private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) { const { latitude, longitude } = exifData; if (!(await this.configCore.hasFeature(FeatureFlag.REVERSE_GEOCODING)) || !longitude || !latitude) { @@ -246,8 +282,11 @@ export class MetadataService { } try { - const { city, state, country } = await this.repository.reverseGeocode({ latitude, longitude }); - Object.assign(exifData, { city, state, country }); + const reverseGeocode = await this.repository.reverseGeocode({ latitude, longitude }); + if (!reverseGeocode) { + return; + } + Object.assign(exifData, reverseGeocode); } catch (error: Error | any) { this.logger.warn( `Unable to run reverse geocoding due to ${error} for asset ${asset.id} at ${asset.originalPath}`, @@ -338,8 +377,17 @@ export class MetadataService { asset: AssetEntity, ): Promise<{ exifData: ExifEntityWithoutGeocodeAndTypeOrm; tags: ImmichTags }> { const stats = await this.storageRepository.stat(asset.originalPath); - const mediaTags = await this.repository.getExifTags(asset.originalPath); - const sidecarTags = asset.sidecarPath ? await this.repository.getExifTags(asset.sidecarPath) : null; + const mediaTags = await this.repository.readTags(asset.originalPath); + const sidecarTags = asset.sidecarPath ? await this.repository.readTags(asset.sidecarPath) : null; + + // ensure date from sidecar is used if present + const hasDateOverride = !!this.getDateTimeOriginal(sidecarTags); + if (mediaTags && hasDateOverride) { + for (const tag of EXIF_DATE_TAGS) { + delete mediaTags[tag]; + } + } + const tags = { ...mediaTags, ...sidecarTags }; this.logger.verbose('Exif Tags', tags); @@ -350,19 +398,7 @@ export class MetadataService { assetId: asset.id, bitsPerSample: this.getBitsPerSample(tags), colorspace: tags.ColorSpace ?? null, - dateTimeOriginal: - exifDate( - firstDateTime(tags as Tags, [ - 'SubSecDateTimeOriginal', - 'DateTimeOriginal', - 'SubSecCreateDate', - 'CreationDate', - 'CreateDate', - 'SubSecMediaCreateDate', - 'MediaCreateDate', - 'DateTimeCreated', - ]), - ) ?? asset.fileCreatedAt, + dateTimeOriginal: this.getDateTimeOriginal(tags) ?? asset.fileCreatedAt, exifImageHeight: validate(tags.ImageHeight), exifImageWidth: validate(tags.ImageWidth), exposureTime: tags.ExposureTime ?? null, @@ -387,6 +423,13 @@ export class MetadataService { }; } + private getDateTimeOriginal(tags: ImmichTags | Tags | null) { + if (!tags) { + return null; + } + return exifDate(firstDateTime(tags as Tags, EXIF_DATE_TAGS)); + } + private getBitsPerSample(tags: ImmichTags): number | null { const bitDepthTags = [ tags.BitsPerSample, diff --git a/server/src/domain/partner/index.ts b/server/src/domain/partner/index.ts index f78ecb1cb..b25925e89 100644 --- a/server/src/domain/partner/index.ts +++ b/server/src/domain/partner/index.ts @@ -1 +1,2 @@ +export * from './partner.dto'; export * from './partner.service'; diff --git a/server/src/domain/partner/partner.dto.ts b/server/src/domain/partner/partner.dto.ts new file mode 100644 index 000000000..17afcad5d --- /dev/null +++ b/server/src/domain/partner/partner.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty } from 'class-validator'; +import { UserResponseDto } from '../user'; + +export class UpdatePartnerDto { + @IsNotEmpty() + inTimeline!: boolean; +} + +export class PartnerResponseDto extends UserResponseDto { + inTimeline?: boolean; +} diff --git a/server/src/domain/partner/partner.service.spec.ts b/server/src/domain/partner/partner.service.spec.ts index 2bc561194..c632cc8da 100644 --- a/server/src/domain/partner/partner.service.spec.ts +++ b/server/src/domain/partner/partner.service.spec.ts @@ -1,16 +1,16 @@ +import { UserAvatarColor } from '@app/infra/entities'; import { BadRequestException } from '@nestjs/common'; import { authStub, newPartnerRepositoryMock, partnerStub } from '@test'; -import { UserResponseDto } from '../index'; -import { IPartnerRepository, PartnerDirection } from '../repositories'; +import { IAccessRepository, IPartnerRepository, PartnerDirection } from '../repositories'; +import { PartnerResponseDto } from './partner.dto'; import { PartnerService } from './partner.service'; const responseDto = { - admin: { + admin: { email: 'admin@test.com', - firstName: 'admin_first_name', + name: 'admin_name', id: 'admin_id', isAdmin: true, - lastName: 'admin_last_name', oauthId: '', profileImagePath: '', shouldChangePassword: false, @@ -20,13 +20,14 @@ const responseDto = { updatedAt: new Date('2021-01-01'), externalPath: null, memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, + inTimeline: true, }, - user1: { + user1: { email: 'immich@test.com', - firstName: 'immich_first_name', + name: 'immich_name', id: 'user-id', isAdmin: false, - lastName: 'immich_last_name', oauthId: '', profileImagePath: '', shouldChangePassword: false, @@ -36,16 +37,19 @@ const responseDto = { updatedAt: new Date('2021-01-01'), externalPath: null, memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, + inTimeline: true, }, }; describe(PartnerService.name, () => { let sut: PartnerService; let partnerMock: jest.Mocked; + let accessMock: jest.Mocked; beforeEach(async () => { partnerMock = newPartnerRepositoryMock(); - sut = new PartnerService(partnerMock); + sut = new PartnerService(partnerMock, accessMock); }); it('should work', () => { diff --git a/server/src/domain/partner/partner.service.ts b/server/src/domain/partner/partner.service.ts index 797938a8d..93600a5c0 100644 --- a/server/src/domain/partner/partner.service.ts +++ b/server/src/domain/partner/partner.service.ts @@ -1,14 +1,22 @@ import { PartnerEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { AccessCore, Permission } from '../access'; import { AuthUserDto } from '../auth'; -import { IPartnerRepository, PartnerDirection, PartnerIds } from '../repositories'; -import { UserResponseDto, mapUser } from '../user'; +import { IAccessRepository, IPartnerRepository, PartnerDirection, PartnerIds } from '../repositories'; +import { mapUser } from '../user'; +import { PartnerResponseDto, UpdatePartnerDto } from './partner.dto'; @Injectable() export class PartnerService { - constructor(@Inject(IPartnerRepository) private repository: IPartnerRepository) {} + private access: AccessCore; + constructor( + @Inject(IPartnerRepository) private repository: IPartnerRepository, + @Inject(IAccessRepository) accessRepository: IAccessRepository, + ) { + this.access = AccessCore.create(accessRepository); + } - async create(authUser: AuthUserDto, sharedWithId: string): Promise { + async create(authUser: AuthUserDto, sharedWithId: string): Promise { const partnerId: PartnerIds = { sharedById: authUser.id, sharedWithId }; const exists = await this.repository.get(partnerId); if (exists) { @@ -29,7 +37,7 @@ export class PartnerService { await this.repository.remove(partner); } - async getAll(authUser: AuthUserDto, direction: PartnerDirection): Promise { + async getAll(authUser: AuthUserDto, direction: PartnerDirection): Promise { const partners = await this.repository.getAll(authUser.id); const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId'; return partners @@ -38,8 +46,22 @@ export class PartnerService { .map((partner) => this.map(partner, direction)); } - private map(partner: PartnerEntity, direction: PartnerDirection): UserResponseDto { + async update(authUser: AuthUserDto, sharedById: string, dto: UpdatePartnerDto): Promise { + await this.access.requirePermission(authUser, Permission.PARTNER_UPDATE, sharedById); + const partnerId: PartnerIds = { sharedById, sharedWithId: authUser.id }; + + const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline }); + return this.map(entity, PartnerDirection.SharedWith); + } + + private map(partner: PartnerEntity, direction: PartnerDirection): PartnerResponseDto { // this is opposite to return the non-me user of the "partner" - return mapUser(direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy); + const user = mapUser( + direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy, + ) as PartnerResponseDto; + + user.inTimeline = partner.inTimeline; + + return user; } } diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index 59b33f4dc..44c20712b 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -183,105 +183,101 @@ describe(PersonService.name, () => { describe('getById', () => { it('should require person.read permission', async () => { personMock.getById.mockResolvedValue(personStub.withName); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw a bad request when person is not found', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should get a person by id', async () => { personMock.getById.mockResolvedValue(personStub.withName); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getById(authStub.admin, 'person-1')).resolves.toEqual(responseDto); expect(personMock.getById).toHaveBeenCalledWith('person-1'); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); describe('getThumbnail', () => { it('should require person.read permission', async () => { personMock.getById.mockResolvedValue(personStub.noName); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); expect(storageMock.createReadStream).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when personId is invalid', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); expect(storageMock.createReadStream).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when person has no thumbnail', async () => { personMock.getById.mockResolvedValue(personStub.noThumbnail); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); expect(storageMock.createReadStream).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should serve the thumbnail', async () => { personMock.getById.mockResolvedValue(personStub.noName); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await sut.getThumbnail(authStub.admin, 'person-1'); expect(storageMock.createReadStream).toHaveBeenCalledWith('/path/to/thumbnail.jpg', 'image/jpeg'); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); describe('getAssets', () => { it('should require person.read permission', async () => { personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.getAssets(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); expect(personMock.getAssets).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it("should return a person's assets", async () => { personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await sut.getAssets(authStub.admin, 'person-1'); expect(personMock.getAssets).toHaveBeenCalledWith('person-1'); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); describe('update', () => { it('should require person.write permission', async () => { personMock.getById.mockResolvedValue(personStub.noName); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).rejects.toBeInstanceOf( BadRequestException, ); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when personId is invalid', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).rejects.toBeInstanceOf( BadRequestException, ); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it("should update a person's name", async () => { personMock.getById.mockResolvedValue(personStub.noName); personMock.update.mockResolvedValue(personStub.withName); personMock.getAssets.mockResolvedValue([assetStub.image]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto); @@ -291,14 +287,14 @@ describe(PersonService.name, () => { name: JobName.SEARCH_INDEX_ASSET, data: { ids: [assetStub.image.id] }, }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it("should update a person's date of birth", async () => { personMock.getById.mockResolvedValue(personStub.noBirthDate); personMock.update.mockResolvedValue(personStub.withBirthDate); personMock.getAssets.mockResolvedValue([assetStub.image]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { birthDate: new Date('1976-06-30') })).resolves.toEqual({ id: 'person-1', @@ -311,14 +307,14 @@ describe(PersonService.name, () => { expect(personMock.getById).toHaveBeenCalledWith('person-1'); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') }); expect(jobMock.queue).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should update a person visibility', async () => { personMock.getById.mockResolvedValue(personStub.hidden); personMock.update.mockResolvedValue(personStub.withName); personMock.getAssets.mockResolvedValue([assetStub.image]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto); @@ -328,15 +324,15 @@ describe(PersonService.name, () => { name: JobName.SEARCH_INDEX_ASSET, data: { ids: [assetStub.image.id] }, }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it("should update a person's thumbnailPath", async () => { personMock.getById.mockResolvedValue(personStub.withName); personMock.update.mockResolvedValue(personStub.withName); personMock.getFacesByIds.mockResolvedValue([faceStub.face1]); - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect( sut.update(authStub.admin, 'person-1', { featureFaceAssetId: faceStub.face1.assetId }), @@ -351,31 +347,31 @@ describe(PersonService.name, () => { }, ]); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: 'person-1' } }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when the face feature assetId is invalid', async () => { personMock.getById.mockResolvedValue(personStub.withName); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { featureFaceAssetId: '-1' })).rejects.toThrow( BadRequestException, ); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); describe('updateAll', () => { it('should throw an error when personId is invalid', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect( sut.updatePeople(authStub.admin, { people: [{ id: 'person-1', name: 'Person 1' }] }), ).resolves.toEqual([{ error: BulkIdErrorReason.UNKNOWN, id: 'person-1', success: false }]); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); @@ -467,6 +463,8 @@ describe(PersonService.name, () => { }); it('should handle no results', async () => { + const start = Date.now(); + machineLearningMock.detectFaces.mockResolvedValue([]); assetMock.getByIds.mockResolvedValue([assetStub.image]); await sut.handleRecognizeFaces({ id: assetStub.image.id }); @@ -485,6 +483,12 @@ describe(PersonService.name, () => { ); expect(personMock.createFace).not.toHaveBeenCalled(); expect(jobMock.queue).not.toHaveBeenCalled(); + + expect(assetMock.upsertJobStatus).toHaveBeenCalledWith({ + assetId: assetStub.image.id, + facesRecognizedAt: expect.any(Date), + }); + expect(assetMock.upsertJobStatus.mock.calls[0][0].facesRecognizedAt?.getTime()).toBeGreaterThan(start); }); it('should match existing people', async () => { @@ -644,7 +648,6 @@ describe(PersonService.name, () => { personMock.getById.mockResolvedValueOnce(personStub.mergePerson); personMock.prepareReassignFaces.mockResolvedValue([]); personMock.delete.mockResolvedValue(personStub.mergePerson); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf( BadRequestException, @@ -655,7 +658,7 @@ describe(PersonService.name, () => { expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should merge two people', async () => { @@ -663,7 +666,8 @@ describe(PersonService.name, () => { personMock.getById.mockResolvedValueOnce(personStub.mergePerson); personMock.prepareReassignFaces.mockResolvedValue([]); personMock.delete.mockResolvedValue(personStub.mergePerson); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ { id: 'person-2', success: true }, @@ -683,14 +687,15 @@ describe(PersonService.name, () => { name: JobName.PERSON_DELETE, data: { id: personStub.mergePerson.id }, }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should delete conflicting faces before merging', async () => { personMock.getById.mockResolvedValue(personStub.primaryPerson); personMock.getById.mockResolvedValue(personStub.mergePerson); personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ { id: 'person-2', success: true }, @@ -705,25 +710,26 @@ describe(PersonService.name, () => { name: JobName.SEARCH_REMOVE_FACE, data: { assetId: assetStub.image.id, personId: personStub.mergePerson.id }, }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when the primary person is not found', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf( BadRequestException, ); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should handle invalid merge ids', async () => { personMock.getById.mockResolvedValueOnce(personStub.primaryPerson); personMock.getById.mockResolvedValueOnce(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ { id: 'person-2', success: false, error: BulkIdErrorReason.NOT_FOUND }, @@ -732,7 +738,7 @@ describe(PersonService.name, () => { expect(personMock.prepareReassignFaces).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should handle an error reassigning faces', async () => { @@ -740,14 +746,15 @@ describe(PersonService.name, () => { personMock.getById.mockResolvedValue(personStub.mergePerson); personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]); personMock.reassignFaces.mockRejectedValue(new Error('update failed')); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ { id: 'person-2', success: false, error: BulkIdErrorReason.UNKNOWN }, ]); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); @@ -755,16 +762,15 @@ describe(PersonService.name, () => { it('should get correct number of person', async () => { personMock.getById.mockResolvedValue(personStub.primaryPerson); personMock.getStatistics.mockResolvedValue(statistics); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getStatistics(authStub.admin, 'person-1')).resolves.toEqual({ assets: 3 }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should require person.read permission', async () => { personMock.getById.mockResolvedValue(personStub.primaryPerson); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.getStatistics(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); }); diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 0d8f9b6b8..3452807f6 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -274,6 +274,11 @@ export class PersonService { } } + await this.assetRepository.upsertJobStatus({ + assetId: asset.id, + facesRecognizedAt: new Date(), + }); + return true; } @@ -370,10 +375,11 @@ export class PersonService { const results: BulkIdResponseDto[] = []; - for (const mergeId of mergeIds) { - const hasPermission = await this.access.hasPermission(authUser, Permission.PERSON_MERGE, mergeId); + const allowedIds = await this.access.checkAccess(authUser, Permission.PERSON_MERGE, mergeIds); - if (!hasPermission) { + for (const mergeId of mergeIds) { + const hasAccess = allowedIds.has(mergeId); + if (!hasAccess) { results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NO_PERMISSION }); continue; } diff --git a/server/src/domain/repositories/access.repository.ts b/server/src/domain/repositories/access.repository.ts index 43b53e605..b6a71daf8 100644 --- a/server/src/domain/repositories/access.repository.ts +++ b/server/src/domain/repositories/access.repository.ts @@ -2,36 +2,42 @@ export const IAccessRepository = 'IAccessRepository'; export interface IAccessRepository { activity: { - hasOwnerAccess(userId: string, albumId: string): Promise; - hasAlbumOwnerAccess(userId: string, albumId: string): Promise; + hasOwnerAccess(userId: string, activityId: string): Promise; + hasAlbumOwnerAccess(userId: string, activityId: string): Promise; + hasCreateAccess(userId: string, albumId: string): Promise; }; + asset: { - hasOwnerAccess(userId: string, assetId: string): Promise; - hasAlbumAccess(userId: string, assetId: string): Promise; - hasPartnerAccess(userId: string, assetId: string): Promise; - hasSharedLinkAccess(sharedLinkId: string, assetId: string): Promise; + checkOwnerAccess(userId: string, assetIds: Set): Promise>; + checkAlbumAccess(userId: string, assetIds: Set): Promise>; + checkPartnerAccess(userId: string, assetIds: Set): Promise>; + checkSharedLinkAccess(sharedLinkId: string, assetIds: Set): Promise>; }; authDevice: { - hasOwnerAccess(userId: string, deviceId: string): Promise; + checkOwnerAccess(userId: string, deviceIds: Set): Promise>; }; album: { - hasOwnerAccess(userId: string, albumId: string): Promise; - hasSharedAlbumAccess(userId: string, albumId: string): Promise; - hasSharedLinkAccess(sharedLinkId: string, albumId: string): Promise; + checkOwnerAccess(userId: string, albumIds: Set): Promise>; + checkSharedAlbumAccess(userId: string, albumIds: Set): Promise>; + checkSharedLinkAccess(sharedLinkId: string, albumIds: Set): Promise>; }; library: { - hasOwnerAccess(userId: string, libraryId: string): Promise; - hasPartnerAccess(userId: string, partnerId: string): Promise; + checkOwnerAccess(userId: string, libraryIds: Set): Promise>; + checkPartnerAccess(userId: string, partnerIds: Set): Promise>; }; timeline: { - hasPartnerAccess(userId: string, partnerId: string): Promise; + checkPartnerAccess(userId: string, partnerIds: Set): Promise>; }; person: { - hasOwnerAccess(userId: string, personId: string): Promise; + checkOwnerAccess(userId: string, personIds: Set): Promise>; + }; + + partner: { + checkUpdateAccess(userId: string, partnerIds: Set): Promise>; }; } diff --git a/server/src/domain/repositories/album.repository.ts b/server/src/domain/repositories/album.repository.ts index d3ca62da1..10b789b4b 100644 --- a/server/src/domain/repositories/album.repository.ts +++ b/server/src/domain/repositories/album.repository.ts @@ -5,6 +5,8 @@ export const IAlbumRepository = 'IAlbumRepository'; export interface AlbumAssetCount { albumId: string; assetCount: number; + startDate: Date | undefined; + endDate: Date | undefined; } export interface AlbumInfoOptions { @@ -30,7 +32,7 @@ export interface IAlbumRepository { hasAsset(asset: AlbumAsset): Promise; removeAsset(assetId: string): Promise; removeAssets(assets: AlbumAssets): Promise; - getAssetCountForIds(ids: string[]): Promise; + getMetadataForIds(ids: string[]): Promise; getInvalidThumbnail(): Promise; getOwned(ownerId: string): Promise; getShared(ownerId: string): Promise; diff --git a/server/src/domain/repositories/asset.repository.ts b/server/src/domain/repositories/asset.repository.ts index 4b11cb55a..a42952958 100644 --- a/server/src/domain/repositories/asset.repository.ts +++ b/server/src/domain/repositories/asset.repository.ts @@ -1,4 +1,4 @@ -import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; +import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities'; import { FindOptionsRelations } from 'typeorm'; import { Paginated, PaginationOptions } from '../domain.util'; @@ -11,11 +11,58 @@ export interface AssetStatsOptions { } export interface AssetSearchOptions { - isVisible?: boolean; - trashedBefore?: Date; + id?: string; + libraryId?: string; + deviceAssetId?: string; + deviceId?: string; + ownerId?: string; type?: AssetType; - order?: 'ASC' | 'DESC'; + checksum?: Buffer; + + isArchived?: boolean; + isEncoded?: boolean; + isExternal?: boolean; + isFavorite?: boolean; + isMotion?: boolean; + isOffline?: boolean; + isReadOnly?: boolean; + isVisible?: boolean; + withDeleted?: boolean; + withStacked?: boolean; + withExif?: boolean; + withPeople?: boolean; + + createdBefore?: Date; + createdAfter?: Date; + updatedBefore?: Date; + updatedAfter?: Date; + trashedBefore?: Date; + trashedAfter?: Date; + takenBefore?: Date; + takenAfter?: Date; + + originalFileName?: string; + originalPath?: string; + resizePath?: string; + webpPath?: string; + encodedVideoPath?: string; + + city?: string; + state?: string; + country?: string; + make?: string; + model?: string; + lensModel?: string; + + /** defaults to 'DESC' */ + order?: 'ASC' | 'DESC'; + + /** defaults to 1 */ + page?: number; + + /** defaults to 250 */ + size?: number; } export interface LivePhotoSearchOptions { @@ -65,7 +112,7 @@ export interface TimeBucketOptions { isTrashed?: boolean; albumId?: string; personId?: string; - userId?: string; + userIds?: string[]; withStacked?: boolean; } @@ -115,6 +162,7 @@ export interface IAssetRepository { getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise; deleteAll(ownerId: string): Promise; getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated; + getAllByDeviceId(userId: string, deviceId: string): Promise; updateAll(ids: string[], options: Partial): Promise; save(asset: Pick & Partial): Promise; remove(asset: AssetEntity): Promise; @@ -126,4 +174,6 @@ export interface IAssetRepository { getTimeBuckets(options: TimeBucketOptions): Promise; getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise; upsertExif(exif: Partial): Promise; + upsertJobStatus(jobStatus: Partial): Promise; + search(options: AssetSearchOptions): Promise; } diff --git a/server/src/domain/repositories/index.ts b/server/src/domain/repositories/index.ts index ff098d8db..f812e6ee5 100644 --- a/server/src/domain/repositories/index.ts +++ b/server/src/domain/repositories/index.ts @@ -20,6 +20,7 @@ export * from './shared-link.repository'; export * from './smart-info.repository'; export * from './storage.repository'; export * from './system-config.repository'; +export * from './system-metadata.repository'; export * from './tag.repository'; export * from './user-token.repository'; export * from './user.repository'; diff --git a/server/src/domain/repositories/job.repository.ts b/server/src/domain/repositories/job.repository.ts index 4b426062f..7b9deabbd 100644 --- a/server/src/domain/repositories/job.repository.ts +++ b/server/src/domain/repositories/job.repository.ts @@ -9,6 +9,7 @@ import { IEntityJob, ILibraryFileJob, ILibraryRefreshJob, + ISidecarWriteJob, } from '../job/job.interface'; export interface JobCounts { @@ -54,11 +55,11 @@ export type JobItem = | { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob } | { name: JobName.METADATA_EXTRACTION; data: IEntityJob } | { name: JobName.LINK_LIVE_PHOTOS; data: IEntityJob } - // Sidecar Scanning | { name: JobName.QUEUE_SIDECAR; data: IBaseJob } | { name: JobName.SIDECAR_DISCOVERY; data: IEntityJob } | { name: JobName.SIDECAR_SYNC; data: IEntityJob } + | { name: JobName.SIDECAR_WRITE; data: ISidecarWriteJob } // Object Tagging | { name: JobName.QUEUE_OBJECT_TAGGING; data: IBaseJob } diff --git a/server/src/domain/repositories/metadata.repository.ts b/server/src/domain/repositories/metadata.repository.ts index 0c3b78462..e8d4d1e4e 100644 --- a/server/src/domain/repositories/metadata.repository.ts +++ b/server/src/domain/repositories/metadata.repository.ts @@ -1,5 +1,4 @@ import { Tags } from 'exiftool-vendored'; -import { InitOptions } from 'local-reverse-geocoder'; export const IMetadataRepository = 'IMetadataRepository'; @@ -31,9 +30,9 @@ export interface ImmichTags extends Omit { } export interface IMetadataRepository { - init(options: Partial): Promise; + init(): Promise; teardown(): Promise; - reverseGeocode(point: GeoPoint): Promise; - deleteCache(): Promise; - getExifTags(path: string): Promise; + reverseGeocode(point: GeoPoint): Promise; + readTags(path: string): Promise; + writeTags(path: string, tags: Partial): Promise; } diff --git a/server/src/domain/repositories/partner.repository.ts b/server/src/domain/repositories/partner.repository.ts index fd436932c..f0409b67a 100644 --- a/server/src/domain/repositories/partner.repository.ts +++ b/server/src/domain/repositories/partner.repository.ts @@ -17,4 +17,5 @@ export interface IPartnerRepository { get(partner: PartnerIds): Promise; create(partner: PartnerIds): Promise; remove(entity: PartnerEntity): Promise; + update(entity: Partial): Promise; } diff --git a/server/src/domain/repositories/system-config.repository.ts b/server/src/domain/repositories/system-config.repository.ts index f99979207..d154f6eff 100644 --- a/server/src/domain/repositories/system-config.repository.ts +++ b/server/src/domain/repositories/system-config.repository.ts @@ -3,8 +3,9 @@ import { SystemConfigEntity } from '@app/infra/entities'; export const ISystemConfigRepository = 'ISystemConfigRepository'; export interface ISystemConfigRepository { + fetchStyle(url: string): Promise; load(): Promise; - readFile(filename: string): Promise; + readFile(filename: string): Promise; saveAll(items: SystemConfigEntity[]): Promise; deleteKeys(keys: string[]): Promise; } diff --git a/server/src/domain/repositories/system-metadata.repository.ts b/server/src/domain/repositories/system-metadata.repository.ts new file mode 100644 index 000000000..4d571953b --- /dev/null +++ b/server/src/domain/repositories/system-metadata.repository.ts @@ -0,0 +1,8 @@ +import { SystemMetadata } from '@app/infra/entities'; + +export const ISystemMetadataRepository = 'ISystemMetadataRepository'; + +export interface ISystemMetadataRepository { + get(key: T): Promise; + set(key: T, value: SystemMetadata[T]): Promise; +} diff --git a/server/src/domain/repositories/user.repository.ts b/server/src/domain/repositories/user.repository.ts index c4647d571..76060c327 100644 --- a/server/src/domain/repositories/user.repository.ts +++ b/server/src/domain/repositories/user.repository.ts @@ -6,8 +6,7 @@ export interface UserListFilter { export interface UserStatsQueryResponse { userId: string; - userFirstName: string; - userLastName: string; + userName: string; photos: number; videos: number; usage: number; diff --git a/server/src/domain/server-info/server-info.dto.ts b/server/src/domain/server-info/server-info.dto.ts index 846458223..0223a58fa 100644 --- a/server/src/domain/server-info/server-info.dto.ts +++ b/server/src/domain/server-info/server-info.dto.ts @@ -38,9 +38,7 @@ export class UsageByUserDto { @ApiProperty({ type: 'string' }) userId!: string; @ApiProperty({ type: 'string' }) - userFirstName!: string; - @ApiProperty({ type: 'string' }) - userLastName!: string; + userName!: string; @ApiProperty({ type: 'integer' }) photos!: number; @ApiProperty({ type: 'integer' }) @@ -85,7 +83,6 @@ export class ServerThemeDto extends SystemConfigThemeDto {} export class ServerConfigDto { oauthButtonText!: string; loginPageMessage!: string; - mapTileUrl!: string; @ApiProperty({ type: 'integer' }) trashDays!: number; isInitialized!: boolean; diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/domain/server-info/server-info.service.spec.ts index f4025a331..74aab12c7 100644 --- a/server/src/domain/server-info/server-info.service.spec.ts +++ b/server/src/domain/server-info/server-info.service.spec.ts @@ -185,7 +185,6 @@ describe(ServerInfoService.name, () => { loginPageMessage: '', oauthButtonText: 'Login with OAuth', trashDays: 30, - mapTileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', }); expect(configMock.load).toHaveBeenCalled(); }); @@ -196,24 +195,21 @@ describe(ServerInfoService.name, () => { userMock.getUserStats.mockResolvedValue([ { userId: 'user1', - userFirstName: '1', - userLastName: 'User', + userName: '1 User', photos: 10, videos: 11, usage: 12345, }, { userId: 'user2', - userFirstName: '2', - userLastName: 'User', + userName: '2 User', photos: 10, videos: 20, usage: 123456, }, { userId: 'user3', - userFirstName: '3', - userLastName: 'User', + userName: '3 User', photos: 100, videos: 0, usage: 987654, @@ -228,25 +224,22 @@ describe(ServerInfoService.name, () => { { photos: 10, usage: 12345, - userFirstName: '1', + userName: '1 User', userId: 'user1', - userLastName: 'User', videos: 11, }, { photos: 10, usage: 123456, - userFirstName: '2', + userName: '2 User', userId: 'user2', - userLastName: 'User', videos: 20, }, { photos: 100, usage: 987654, - userFirstName: '3', + userName: '3 User', userId: 'user3', - userLastName: 'User', videos: 0, }, ], diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/domain/server-info/server-info.service.ts index dc2efd416..c215709eb 100644 --- a/server/src/domain/server-info/server-info.service.ts +++ b/server/src/domain/server-info/server-info.service.ts @@ -85,7 +85,6 @@ export class ServerInfoService { return { loginPageMessage, - mapTileUrl: config.map.tileUrl, trashDays: config.trash.days, oauthButtonText: config.oauth.buttonText, isInitialized, @@ -99,8 +98,7 @@ export class ServerInfoService { for (const user of userStats) { const usage = new UsageByUserDto(); usage.userId = user.userId; - usage.userFirstName = user.userFirstName; - usage.userLastName = user.userLastName; + usage.userName = user.userName; usage.photos = user.photos; usage.videos = user.videos; usage.usage = user.usage; diff --git a/server/src/domain/shared-link/shared-link.service.spec.ts b/server/src/domain/shared-link/shared-link.service.spec.ts index 863e3a353..bfc74e824 100644 --- a/server/src/domain/shared-link/shared-link.service.spec.ts +++ b/server/src/domain/shared-link/shared-link.service.spec.ts @@ -11,7 +11,6 @@ import { sharedLinkResponseStub, sharedLinkStub, } from '@test'; -import { when } from 'jest-when'; import _ from 'lodash'; import { AssetIdErrorReason } from '../asset'; import { ICryptoRepository, ISharedLinkRepository } from '../repositories'; @@ -97,7 +96,6 @@ describe(SharedLinkService.name, () => { }); it('should not allow non-owners to create album shared links', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); await expect( sut.create(authStub.admin, { type: SharedLinkType.ALBUM, assetIds: [], albumId: 'album-1' }), ).rejects.toBeInstanceOf(BadRequestException); @@ -110,19 +108,21 @@ describe(SharedLinkService.name, () => { }); it('should require asset ownership to make an individual shared link', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(false); await expect( sut.create(authStub.admin, { type: SharedLinkType.INDIVIDUAL, assetIds: ['asset-1'] }), ).rejects.toBeInstanceOf(BadRequestException); }); it('should create an album shared link', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id])); shareMock.create.mockResolvedValue(sharedLinkStub.valid); await sut.create(authStub.admin, { type: SharedLinkType.ALBUM, albumId: albumStub.oneAsset.id }); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, albumStub.oneAsset.id); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith( + authStub.admin.id, + new Set([albumStub.oneAsset.id]), + ); expect(shareMock.create).toHaveBeenCalledWith({ type: SharedLinkType.ALBUM, userId: authStub.admin.id, @@ -138,7 +138,7 @@ describe(SharedLinkService.name, () => { }); it('should create an individual shared link', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); shareMock.create.mockResolvedValue(sharedLinkStub.individual); await sut.create(authStub.admin, { @@ -149,7 +149,7 @@ describe(SharedLinkService.name, () => { allowUpload: true, }); - expect(accessMock.asset.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, assetStub.image.id); + expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set([assetStub.image.id])); expect(shareMock.create).toHaveBeenCalledWith({ type: SharedLinkType.INDIVIDUAL, userId: authStub.admin.id, @@ -213,9 +213,7 @@ describe(SharedLinkService.name, () => { it('should add assets to a shared link', async () => { shareMock.get.mockResolvedValue(_.cloneDeep(sharedLinkStub.individual)); shareMock.create.mockResolvedValue(sharedLinkStub.individual); - - when(accessMock.asset.hasOwnerAccess).calledWith(authStub.admin.id, 'asset-2').mockResolvedValue(false); - when(accessMock.asset.hasOwnerAccess).calledWith(authStub.admin.id, 'asset-3').mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-3'])); await expect( sut.addAssets(authStub.admin, 'link-1', { assetIds: [assetStub.image.id, 'asset-2', 'asset-3'] }), @@ -225,7 +223,7 @@ describe(SharedLinkService.name, () => { { assetId: 'asset-3', success: true }, ]); - expect(accessMock.asset.hasOwnerAccess).toHaveBeenCalledTimes(2); + expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledTimes(1); expect(shareMock.update).toHaveBeenCalledWith({ ...sharedLinkStub.individual, assets: [assetStub.image, { id: 'asset-3' }], diff --git a/server/src/domain/shared-link/shared-link.service.ts b/server/src/domain/shared-link/shared-link.service.ts index d3fd89661..bd9aaea6a 100644 --- a/server/src/domain/shared-link/shared-link.service.ts +++ b/server/src/domain/shared-link/shared-link.service.ts @@ -119,15 +119,19 @@ export class SharedLinkService { throw new BadRequestException('Invalid shared link type'); } + const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id)); + const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId)); + const allowedAssetIds = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, notPresentAssetIds); + const results: AssetIdsResponseDto[] = []; for (const assetId of dto.assetIds) { - const hasAsset = sharedLink.assets.find((asset) => asset.id === assetId); + const hasAsset = existingAssetIds.has(assetId); if (hasAsset) { results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE }); continue; } - const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, assetId); + const hasAccess = allowedAssetIds.has(assetId); if (!hasAccess) { results.push({ assetId, success: false, error: AssetIdErrorReason.NO_PERMISSION }); continue; diff --git a/server/src/domain/system-config/dto/system-config-map.dto.ts b/server/src/domain/system-config/dto/system-config-map.dto.ts index 479b1486d..07700d98c 100644 --- a/server/src/domain/system-config/dto/system-config-map.dto.ts +++ b/server/src/domain/system-config/dto/system-config-map.dto.ts @@ -5,5 +5,8 @@ export class SystemConfigMapDto { enabled!: boolean; @IsString() - tileUrl!: string; + lightStyle!: string; + + @IsString() + darkStyle!: string; } diff --git a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts b/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts index be20a02c7..aa224ccc6 100644 --- a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts +++ b/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts @@ -1,12 +1,6 @@ -import { CitiesFile } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsEnum } from 'class-validator'; +import { IsBoolean } from 'class-validator'; export class SystemConfigReverseGeocodingDto { @IsBoolean() enabled!: boolean; - - @IsEnum(CitiesFile) - @ApiProperty({ enum: CitiesFile, enumName: 'CitiesFile' }) - citiesFileOverride!: CitiesFile; } diff --git a/server/src/domain/system-config/system-config-map-theme.dto.ts b/server/src/domain/system-config/system-config-map-theme.dto.ts new file mode 100644 index 000000000..9286d8d23 --- /dev/null +++ b/server/src/domain/system-config/system-config-map-theme.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum } from 'class-validator'; + +export enum MapTheme { + LIGHT = 'light', + DARK = 'dark', +} + +export class MapThemeDto { + @IsEnum(MapTheme) + @ApiProperty({ enum: MapTheme, enumName: 'MapTheme' }) + theme!: MapTheme; +} diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index 4596370a4..bfab4bb4f 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -1,6 +1,5 @@ import { AudioCodec, - CitiesFile, Colorspace, CQMode, SystemConfig, @@ -80,11 +79,11 @@ export const defaults = Object.freeze({ }, map: { enabled: true, - tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + lightStyle: '', + darkStyle: '', }, reverseGeocoding: { enabled: true, - citiesFileOverride: CitiesFile.CITIES_500, }, oauth: { enabled: false, @@ -327,7 +326,7 @@ export class SystemConfigCore { } if (!_.isEmpty(file)) { - throw new Error(`Unknown keys found: ${file}`); + throw new Error(`Unknown keys found: ${JSON.stringify(file)}`); } this.configCache = overrides; diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index 29ed44e91..6ff4ac5c4 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -1,6 +1,5 @@ import { AudioCodec, - CitiesFile, Colorspace, CQMode, SystemConfig, @@ -80,11 +79,11 @@ const updatedConfig = Object.freeze({ }, map: { enabled: true, - tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + lightStyle: '', + darkStyle: '', }, reverseGeocoding: { enabled: true, - citiesFileOverride: CitiesFile.CITIES_500, }, oauth: { autoLaunch: true, @@ -185,7 +184,7 @@ describe(SystemConfigService.name, () => { it('should load the config from a file', async () => { process.env.IMMICH_CONFIG_FILE = 'immich-config.json'; const partialConfig = { ffmpeg: { crf: 30 }, oauth: { autoLaunch: true }, trash: { days: 10 } }; - configMock.readFile.mockResolvedValue(Buffer.from(JSON.stringify(partialConfig))); + configMock.readFile.mockResolvedValue(JSON.stringify(partialConfig)); await expect(sut.getConfig()).resolves.toEqual(updatedConfig); @@ -194,7 +193,7 @@ describe(SystemConfigService.name, () => { it('should accept an empty configuration file', async () => { process.env.IMMICH_CONFIG_FILE = 'immich-config.json'; - configMock.readFile.mockResolvedValue(Buffer.from(JSON.stringify({}))); + configMock.readFile.mockResolvedValue(JSON.stringify({})); await expect(sut.getConfig()).resolves.toEqual(defaults); @@ -204,7 +203,7 @@ describe(SystemConfigService.name, () => { it('should allow underscores in the machine learning url', async () => { process.env.IMMICH_CONFIG_FILE = 'immich-config.json'; const partialConfig = { machineLearning: { url: 'immich_machine_learning' } }; - configMock.readFile.mockResolvedValue(Buffer.from(JSON.stringify(partialConfig))); + configMock.readFile.mockResolvedValue(JSON.stringify(partialConfig)); const config = await sut.getConfig(); expect(config.machineLearning.url).toEqual('immich_machine_learning'); @@ -222,7 +221,7 @@ describe(SystemConfigService.name, () => { for (const test of tests) { it(`should ${test.should}`, async () => { process.env.IMMICH_CONFIG_FILE = 'immich-config.json'; - configMock.readFile.mockResolvedValue(Buffer.from(JSON.stringify(test.config))); + configMock.readFile.mockResolvedValue(JSON.stringify(test.config)); await expect(sut.getConfig()).rejects.toBeInstanceOf(Error); }); @@ -286,7 +285,7 @@ describe(SystemConfigService.name, () => { it('should throw an error if a config file is in use', async () => { process.env.IMMICH_CONFIG_FILE = 'immich-config.json'; - configMock.readFile.mockResolvedValue(Buffer.from(JSON.stringify({}))); + configMock.readFile.mockResolvedValue(JSON.stringify({})); await expect(sut.updateConfig(defaults)).rejects.toBeInstanceOf(BadRequestException); expect(configMock.saveAll).not.toHaveBeenCalled(); }); @@ -305,9 +304,9 @@ describe(SystemConfigService.name, () => { }); }); - describe('getTheme', () => { + describe('getCustomCss', () => { it('should return the default theme', async () => { - await expect(sut.getTheme()).resolves.toEqual(defaults.theme); + await expect(sut.getCustomCss()).resolves.toEqual(defaults.theme.customCss); }); }); }); diff --git a/server/src/domain/system-config/system-config.service.ts b/server/src/domain/system-config/system-config.service.ts index a44f10c09..c81c462e8 100644 --- a/server/src/domain/system-config/system-config.service.ts +++ b/server/src/domain/system-config/system-config.service.ts @@ -1,7 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { JobName } from '../job'; import { CommunicationEvent, ICommunicationRepository, IJobRepository, ISystemConfigRepository } from '../repositories'; -import { SystemConfigThemeDto } from './dto/system-config-theme.dto'; import { SystemConfigDto, mapConfig } from './dto/system-config.dto'; import { SystemConfigTemplateStorageOptionDto } from './response-dto/system-config-template-storage-option.dto'; import { @@ -20,7 +19,7 @@ import { SystemConfigCore, SystemConfigValidator } from './system-config.core'; export class SystemConfigService { private core: SystemConfigCore; constructor( - @Inject(ISystemConfigRepository) repository: ISystemConfigRepository, + @Inject(ISystemConfigRepository) private repository: ISystemConfigRepository, @Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, ) { @@ -31,11 +30,6 @@ export class SystemConfigService { return this.core.config$; } - async getTheme(): Promise { - const { theme } = await this.core.getConfig(); - return theme; - } - async getConfig(): Promise { const config = await this.core.getConfig(); return mapConfig(config); @@ -76,4 +70,20 @@ export class SystemConfigService { return options; } + + async getMapStyle(theme: 'light' | 'dark') { + const { map } = await this.getConfig(); + const styleUrl = theme === 'dark' ? map.darkStyle : map.lightStyle; + + if (styleUrl) { + return this.repository.fetchStyle(styleUrl); + } + + return JSON.parse(await this.repository.readFile(`./resources/style-${theme}.json`)); + } + + async getCustomCss(): Promise { + const { theme } = await this.core.getConfig(); + return theme.customCss; + } } diff --git a/server/src/domain/user/dto/create-user.dto.spec.ts b/server/src/domain/user/dto/create-user.dto.spec.ts index 492d44c36..4e571d38a 100644 --- a/server/src/domain/user/dto/create-user.dto.spec.ts +++ b/server/src/domain/user/dto/create-user.dto.spec.ts @@ -7,8 +7,7 @@ describe('create user DTO', () => { const params: Partial = { email: undefined, password: 'password', - firstName: 'first name', - lastName: 'last name', + name: 'name', }; let dto: CreateUserDto = plainToInstance(CreateUserDto, params); let errors = await validate(dto); @@ -31,8 +30,7 @@ describe('create user DTO', () => { const dto = plainToInstance(CreateUserDto, { email: someEmail, password: 'some password', - firstName: 'some first name', - lastName: 'some last name', + name: 'some name', }); const errors = await validate(dto); expect(errors).toHaveLength(0); @@ -48,8 +46,7 @@ describe('create admin DTO', () => { isAdmin: true, email: someEmail, password: 'some password', - firstName: 'some first name', - lastName: 'some last name', + name: 'some name', }); const errors = await validate(dto); expect(errors).toHaveLength(0); @@ -64,8 +61,7 @@ describe('create user oauth DTO', () => { const dto = plainToInstance(CreateUserOAuthDto, { email: someEmail, oauthId: 'some oauth id', - firstName: 'some first name', - lastName: 'some last name', + name: 'some name', }); const errors = await validate(dto); expect(errors).toHaveLength(0); diff --git a/server/src/domain/user/dto/create-user.dto.ts b/server/src/domain/user/dto/create-user.dto.ts index 2a5f659ef..b1090d9c0 100644 --- a/server/src/domain/user/dto/create-user.dto.ts +++ b/server/src/domain/user/dto/create-user.dto.ts @@ -13,11 +13,7 @@ export class CreateUserDto { @IsNotEmpty() @IsString() - firstName!: string; - - @IsNotEmpty() - @IsString() - lastName!: string; + name!: string; @Optional({ nullable: true }) @IsString() @@ -45,10 +41,7 @@ export class CreateAdminDto { password!: string; @IsNotEmpty() - firstName!: string; - - @IsNotEmpty() - lastName!: string; + name!: string; } export class CreateUserOAuthDto { @@ -59,7 +52,5 @@ export class CreateUserOAuthDto { @IsNotEmpty() oauthId!: string; - firstName?: string; - - lastName?: string; + name?: string; } diff --git a/server/src/domain/user/dto/update-user.dto.ts b/server/src/domain/user/dto/update-user.dto.ts index e993ae80f..a71c0e21a 100644 --- a/server/src/domain/user/dto/update-user.dto.ts +++ b/server/src/domain/user/dto/update-user.dto.ts @@ -1,6 +1,7 @@ +import { UserAvatarColor } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsEmail, IsNotEmpty, IsString, IsUUID } from 'class-validator'; +import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsString, IsUUID } from 'class-validator'; import { Optional, toEmail, toSanitized } from '../../domain.util'; export class UpdateUserDto { @@ -17,12 +18,7 @@ export class UpdateUserDto { @Optional() @IsString() @IsNotEmpty() - firstName?: string; - - @Optional() - @IsString() - @IsNotEmpty() - lastName?: string; + name?: string; @Optional() @IsString() @@ -49,4 +45,9 @@ export class UpdateUserDto { @Optional() @IsBoolean() memoriesEnabled?: boolean; + + @Optional() + @IsEnum(UserAvatarColor) + @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + avatarColor?: UserAvatarColor; } diff --git a/server/src/domain/user/response-dto/user-response.dto.ts b/server/src/domain/user/response-dto/user-response.dto.ts index b9f990378..7b6aef191 100644 --- a/server/src/domain/user/response-dto/user-response.dto.ts +++ b/server/src/domain/user/response-dto/user-response.dto.ts @@ -1,11 +1,26 @@ -import { UserEntity } from '@app/infra/entities'; +import { UserAvatarColor, UserEntity } from '@app/infra/entities'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum } from 'class-validator'; + +export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => { + const values = Object.values(UserAvatarColor); + const randomIndex = Math.floor( + user.email + .split('') + .map((letter) => letter.charCodeAt(0)) + .reduce((a, b) => a + b, 0) % values.length, + ); + return values[randomIndex] as UserAvatarColor; +}; export class UserDto { id!: string; - firstName!: string; - lastName!: string; + name!: string; email!: string; profileImagePath!: string; + @IsEnum(UserAvatarColor) + @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) + avatarColor!: UserAvatarColor; } export class UserResponseDto extends UserDto { @@ -24,9 +39,9 @@ export const mapSimpleUser = (entity: UserEntity): UserDto => { return { id: entity.id, email: entity.email, - firstName: entity.firstName, - lastName: entity.lastName, + name: entity.name, profileImagePath: entity.profileImagePath, + avatarColor: entity.avatarColor ?? getRandomAvatarColor(entity), }; }; diff --git a/server/src/domain/user/user.core.ts b/server/src/domain/user/user.core.ts index 431caa8e1..c8e77a500 100644 --- a/server/src/domain/user/user.core.ts +++ b/server/src/domain/user/user.core.ts @@ -98,7 +98,6 @@ export class UserCore { if (payload.storageLabel) { payload.storageLabel = sanitize(payload.storageLabel); } - const userEntity = await this.userRepository.create(payload); await this.libraryRepository.create({ owner: { id: userEntity.id } as UserEntity, diff --git a/server/src/domain/user/user.service.spec.ts b/server/src/domain/user/user.service.spec.ts index 1f9918fec..04b4206ca 100644 --- a/server/src/domain/user/user.service.spec.ts +++ b/server/src/domain/user/user.service.spec.ts @@ -289,8 +289,7 @@ describe(UserService.name, () => { await expect( sut.create({ email: 'john_smith@email.com', - firstName: 'John', - lastName: 'Smith', + name: 'John Smith', password: 'password', }), ).rejects.toBeInstanceOf(BadRequestException); @@ -303,8 +302,7 @@ describe(UserService.name, () => { await expect( sut.create({ email: userStub.user1.email, - firstName: userStub.user1.firstName, - lastName: userStub.user1.lastName, + name: userStub.user1.name, password: 'password', storageLabel: 'label', }), @@ -313,8 +311,7 @@ describe(UserService.name, () => { expect(userMock.getAdmin).toBeCalled(); expect(userMock.create).toBeCalledWith({ email: userStub.user1.email, - firstName: userStub.user1.firstName, - lastName: userStub.user1.lastName, + name: userStub.user1.name, storageLabel: 'label', password: expect.anything(), }); @@ -326,17 +323,52 @@ describe(UserService.name, () => { const file = { path: '/profile/path' } as Express.Multer.File; userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); - await sut.createProfileImage(userStub.admin, file); - - expect(userMock.update).toHaveBeenCalledWith(userStub.admin.id, { profileImagePath: file.path }); + await expect(sut.createProfileImage(userStub.admin, file)).rejects.toThrowError(BadRequestException); }); it('should throw an error if the user profile could not be updated with the new image', async () => { const file = { path: '/profile/path' } as Express.Multer.File; + userMock.get.mockResolvedValue(userStub.profilePath); userMock.update.mockRejectedValue(new InternalServerErrorException('mocked error')); await expect(sut.createProfileImage(userStub.admin, file)).rejects.toThrowError(InternalServerErrorException); }); + + it('should delete the previous profile image', async () => { + const file = { path: '/profile/path' } as Express.Multer.File; + userMock.get.mockResolvedValue(userStub.profilePath); + const files = [userStub.profilePath.profileImagePath]; + userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); + + await sut.createProfileImage(userStub.admin, file); + await expect(jobMock.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]); + }); + + it('should not delete the profile image if it has not been set', async () => { + const file = { path: '/profile/path' } as Express.Multer.File; + userMock.get.mockResolvedValue(userStub.admin); + userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path }); + + await sut.createProfileImage(userStub.admin, file); + expect(jobMock.queue).not.toHaveBeenCalled(); + }); + }); + + describe('deleteProfileImage', () => { + it('should send an http error has no profile image', async () => { + userMock.get.mockResolvedValue(userStub.admin); + + await expect(sut.deleteProfileImage(userStub.admin)).rejects.toBeInstanceOf(BadRequestException); + expect(jobMock.queue).not.toHaveBeenCalled(); + }); + + it('should delete the profile image if user has one', async () => { + userMock.get.mockResolvedValue(userStub.profilePath); + const files = [userStub.profilePath.profileImagePath]; + + await sut.deleteProfileImage(userStub.admin); + await expect(jobMock.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]); + }); }); describe('getUserProfileImage', () => { diff --git a/server/src/domain/user/user.service.ts b/server/src/domain/user/user.service.ts index a155d401d..3232a6f94 100644 --- a/server/src/domain/user/user.service.ts +++ b/server/src/domain/user/user.service.ts @@ -93,10 +93,23 @@ export class UserService { authUser: AuthUserDto, fileInfo: Express.Multer.File, ): Promise { + const { profileImagePath: oldpath } = await this.findOrFail(authUser.id, { withDeleted: false }); const updatedUser = await this.userRepository.update(authUser.id, { profileImagePath: fileInfo.path }); + if (oldpath !== '') { + await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } }); + } return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath); } + async deleteProfileImage(authUser: AuthUserDto): Promise { + const user = await this.findOrFail(authUser.id, { withDeleted: false }); + if (user.profileImagePath === '') { + throw new BadRequestException("Can't delete a missing profile Image"); + } + await this.userRepository.update(authUser.id, { profileImagePath: '' }); + await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [user.profileImagePath] } }); + } + async getProfileImage(id: string): Promise { const user = await this.findOrFail(id, {}); if (!user.profileImagePath) { @@ -111,7 +124,7 @@ export class UserService { throw new BadRequestException('Admin account does not exist'); } - const providedPassword = await ask(admin); + const providedPassword = await ask(mapUser(admin)); const password = providedPassword || randomBytes(24).toString('base64').replace(/\W/g, ''); await this.userCore.updateUser(admin, admin.id, { password }); diff --git a/server/src/admin-cli/app.module.ts b/server/src/immich-admin/app.module.ts similarity index 100% rename from server/src/admin-cli/app.module.ts rename to server/src/immich-admin/app.module.ts diff --git a/server/src/admin-cli/commands/list-users.command.ts b/server/src/immich-admin/commands/list-users.command.ts similarity index 100% rename from server/src/admin-cli/commands/list-users.command.ts rename to server/src/immich-admin/commands/list-users.command.ts diff --git a/server/src/admin-cli/commands/password-login.ts b/server/src/immich-admin/commands/password-login.ts similarity index 89% rename from server/src/admin-cli/commands/password-login.ts rename to server/src/immich-admin/commands/password-login.ts index 8cd1276bc..c846ba2f4 100644 --- a/server/src/admin-cli/commands/password-login.ts +++ b/server/src/immich-admin/commands/password-login.ts @@ -15,7 +15,7 @@ export class EnablePasswordLoginCommand extends CommandRunner { const config = await this.configService.getConfig(); config.passwordLogin.enabled = true; await this.configService.updateConfig(config); - await axios.post('http://localhost:3001/refresh-config'); + await axios.post('http://localhost:3001/api/refresh-config'); console.log('Password login has been enabled.'); } } @@ -33,7 +33,7 @@ export class DisablePasswordLoginCommand extends CommandRunner { const config = await this.configService.getConfig(); config.passwordLogin.enabled = false; await this.configService.updateConfig(config); - await axios.post('http://localhost:3001/refresh-config'); + await axios.post('http://localhost:3001/api/refresh-config'); console.log('Password login has been disabled.'); } } diff --git a/server/src/admin-cli/commands/reset-admin-password.command.ts b/server/src/immich-admin/commands/reset-admin-password.command.ts similarity index 93% rename from server/src/admin-cli/commands/reset-admin-password.command.ts rename to server/src/immich-admin/commands/reset-admin-password.command.ts index f28780a7d..af36c590c 100644 --- a/server/src/admin-cli/commands/reset-admin-password.command.ts +++ b/server/src/immich-admin/commands/reset-admin-password.command.ts @@ -15,12 +15,12 @@ export class ResetAdminPasswordCommand extends CommandRunner { async run(): Promise { const ask = (admin: UserResponseDto) => { - const { id, oauthId, email, firstName, lastName } = admin; + const { id, oauthId, email, name } = admin; console.log(`Found Admin: - ID=${id} - OAuth ID=${oauthId} - Email=${email} -- Name=${firstName} ${lastName}`); +- Name=${name}`); return this.inquirer.ask<{ password: string }>('prompt-password', undefined).then(({ password }) => password); }; diff --git a/server/src/admin-cli/constants.ts b/server/src/immich-admin/constants.ts similarity index 100% rename from server/src/admin-cli/constants.ts rename to server/src/immich-admin/constants.ts diff --git a/server/src/admin-cli/main.ts b/server/src/immich-admin/main.ts similarity index 100% rename from server/src/admin-cli/main.ts rename to server/src/immich-admin/main.ts diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/immich/api-v1/asset/asset.controller.ts index 24127e873..ad17ccf31 100644 --- a/server/src/immich/api-v1/asset/asset.controller.ts +++ b/server/src/immich/api-v1/asset/asset.controller.ts @@ -14,7 +14,7 @@ import { UseInterceptors, ValidationPipe, } from '@nestjs/common'; -import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { Response as Res } from 'express'; import { AuthUser, Authenticated, SharedLinkRoute } from '../../app.guard'; import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto'; @@ -24,10 +24,9 @@ import { AssetService } from './asset.service'; import { AssetBulkUploadCheckDto } from './dto/asset-check.dto'; import { AssetSearchDto } from './dto/asset-search.dto'; import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; -import { CreateAssetDto, ImportAssetDto } from './dto/create-asset.dto'; +import { CreateAssetDto } from './dto/create-asset.dto'; import { DeviceIdDto } from './dto/device-id.dto'; import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto'; -import { SearchAssetDto } from './dto/search-asset.dto'; import { ServeFileDto } from './dto/serve-file.dto'; import { AssetBulkUploadCheckResponseDto } from './response-dto/asset-check-response.dto'; import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto'; @@ -82,20 +81,6 @@ export class AssetController { return responseDto; } - @Post('import') - async importFile( - @AuthUser() authUser: AuthUserDto, - @Body(new ValidationPipe({ transform: true })) dto: ImportAssetDto, - @Response({ passthrough: true }) res: Res, - ): Promise { - const responseDto = await this.assetService.importFile(authUser, dto); - if (responseDto.duplicate) { - res.status(200); - } - - return responseDto; - } - @SharedLinkRoute() @Get('/file/:id') @ApiOkResponse({ @@ -144,15 +129,6 @@ export class AssetController { return this.assetService.getAssetSearchTerm(authUser); } - @Post('/search') - @HttpCode(HttpStatus.OK) - searchAsset( - @AuthUser() authUser: AuthUserDto, - @Body(ValidationPipe) dto: SearchAssetDto, - ): Promise { - return this.assetService.searchAsset(authUser, dto); - } - /** * Get all AssetEntity belong to the user */ @@ -171,9 +147,10 @@ export class AssetController { } /** - * Get all asset of a device that are in the database, ID only. + * @deprecated Use /asset/device/:deviceId instead - Remove at 1.92 release */ @Get('/:deviceId') + @ApiOperation({ deprecated: true, summary: 'Use /asset/device/:deviceId instead - Remove in 1.92 release' }) getUserAssetsByDeviceId(@AuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) { return this.assetService.getUserAssetsByDeviceId(authUser, deviceId); } diff --git a/server/src/immich/api-v1/asset/asset.core.ts b/server/src/immich/api-v1/asset/asset.core.ts index 9f5691b22..d6ce0efad 100644 --- a/server/src/immich/api-v1/asset/asset.core.ts +++ b/server/src/immich/api-v1/asset/asset.core.ts @@ -2,7 +2,7 @@ import { AuthUserDto, IJobRepository, JobName, mimeTypes, UploadFile } from '@ap import { AssetEntity } from '@app/infra/entities'; import { parse } from 'node:path'; import { IAssetRepository } from './asset-repository'; -import { CreateAssetDto, ImportAssetDto } from './dto/create-asset.dto'; +import { CreateAssetDto } from './dto/create-asset.dto'; export class AssetCore { constructor( @@ -12,7 +12,7 @@ export class AssetCore { async create( authUser: AuthUserDto, - dto: (CreateAssetDto | ImportAssetDto) & { libraryId: string }, + dto: CreateAssetDto & { libraryId: string }, file: UploadFile, livePhotoAssetId?: string, sidecarPath?: string, diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts index a293a7875..9b1e02b9c 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/server/src/immich/api-v1/asset/asset.service.spec.ts @@ -1,4 +1,4 @@ -import { ICryptoRepository, IJobRepository, ILibraryRepository, IStorageRepository, JobName } from '@app/domain'; +import { IJobRepository, ILibraryRepository, JobName } from '@app/domain'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; import { BadRequestException } from '@nestjs/common'; import { @@ -7,13 +7,11 @@ import { authStub, fileStub, newAccessRepositoryMock, - newCryptoRepositoryMock, newJobRepositoryMock, newLibraryRepositoryMock, - newStorageRepositoryMock, } from '@test'; import { when } from 'jest-when'; -import { QueryFailedError, Repository } from 'typeorm'; +import { QueryFailedError } from 'typeorm'; import { IAssetRepository } from './asset-repository'; import { AssetService } from './asset.service'; import { CreateAssetDto } from './dto/create-asset.dto'; @@ -85,12 +83,9 @@ const _getAssets = () => { describe('AssetService', () => { let sut: AssetService; - let a: Repository; // TO BE DELETED AFTER FINISHED REFACTORING let accessMock: IAccessRepositoryMock; let assetRepositoryMock: jest.Mocked; - let cryptoMock: jest.Mocked; let jobMock: jest.Mocked; - let storageMock: jest.Mocked; let libraryMock: jest.Mocked; beforeEach(() => { @@ -110,12 +105,10 @@ describe('AssetService', () => { }; accessMock = newAccessRepositoryMock(); - cryptoMock = newCryptoRepositoryMock(); jobMock = newJobRepositoryMock(); - storageMock = newStorageRepositoryMock(); libraryMock = newLibraryRepositoryMock(); - sut = new AssetService(accessMock, assetRepositoryMock, a, cryptoMock, jobMock, libraryMock, storageMock); + sut = new AssetService(accessMock, assetRepositoryMock, jobMock, libraryMock); when(assetRepositoryMock.get) .calledWith(assetStub.livePhotoStillAsset.id) @@ -137,7 +130,7 @@ describe('AssetService', () => { const dto = _getCreateAssetDto(); assetRepositoryMock.create.mockResolvedValue(assetEntity); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' }); @@ -157,7 +150,7 @@ describe('AssetService', () => { assetRepositoryMock.create.mockRejectedValue(error); assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' }); @@ -165,7 +158,6 @@ describe('AssetService', () => { name: JobName.DELETE_FILES, data: { files: ['fake_path/asset_1.jpeg', undefined, undefined] }, }); - expect(storageMock.moveFile).not.toHaveBeenCalled(); }); it('should handle a live photo', async () => { @@ -175,7 +167,7 @@ describe('AssetService', () => { assetRepositoryMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); assetRepositoryMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); await expect( sut.uploadFile(authStub.user1, dto, fileStub.livePhotoStill, fileStub.livePhotoMotion), @@ -238,97 +230,51 @@ describe('AssetService', () => { }); }); - describe('importFile', () => { - it('should handle a file import', async () => { - assetRepositoryMock.create.mockResolvedValue(assetStub.image); - storageMock.checkFileExists.mockResolvedValue(true); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); - - await expect( - sut.importFile(authStub.external1, { - ..._getCreateAssetDto(), - assetPath: '/data/user1/fake_path/asset_1.jpeg', - isReadOnly: true, - libraryId: 'library-id', - }), - ).resolves.toEqual({ duplicate: false, id: 'asset-id' }); - - expect(assetRepositoryMock.create).toHaveBeenCalled(); - }); - - it('should handle a duplicate if originalPath already exists', async () => { - const error = new QueryFailedError('', [], ''); - (error as any).constraint = ASSET_CHECKSUM_CONSTRAINT; - - assetRepositoryMock.create.mockRejectedValue(error); - assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([assetStub.image]); - storageMock.checkFileExists.mockResolvedValue(true); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); - cryptoMock.hashFile.mockResolvedValue(Buffer.from('file hash', 'utf8')); - - await expect( - sut.importFile(authStub.external1, { - ..._getCreateAssetDto(), - assetPath: '/data/user1/fake_path/asset_1.jpeg', - isReadOnly: true, - libraryId: 'library-id', - }), - ).resolves.toEqual({ duplicate: true, id: 'asset-id' }); - - expect(assetRepositoryMock.create).toHaveBeenCalled(); - }); - }); - describe('getAssetById', () => { it('should allow owner access', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(true); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); assetRepositoryMock.getById.mockResolvedValue(assetStub.image); await sut.getAssetById(authStub.admin, assetStub.image.id); - expect(accessMock.asset.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, assetStub.image.id); + expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set([assetStub.image.id])); }); it('should allow shared link access', async () => { - accessMock.asset.hasSharedLinkAccess.mockResolvedValue(true); + accessMock.asset.checkSharedLinkAccess.mockResolvedValue(new Set([assetStub.image.id])); assetRepositoryMock.getById.mockResolvedValue(assetStub.image); await sut.getAssetById(authStub.adminSharedLink, assetStub.image.id); - expect(accessMock.asset.hasSharedLinkAccess).toHaveBeenCalledWith( + expect(accessMock.asset.checkSharedLinkAccess).toHaveBeenCalledWith( authStub.adminSharedLink.sharedLinkId, - assetStub.image.id, + new Set([assetStub.image.id]), ); }); it('should allow partner sharing access', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(false); - accessMock.asset.hasPartnerAccess.mockResolvedValue(true); + accessMock.asset.checkPartnerAccess.mockResolvedValue(new Set([assetStub.image.id])); assetRepositoryMock.getById.mockResolvedValue(assetStub.image); await sut.getAssetById(authStub.admin, assetStub.image.id); - expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, assetStub.image.id); + expect(accessMock.asset.checkPartnerAccess).toHaveBeenCalledWith( + authStub.admin.id, + new Set([assetStub.image.id]), + ); }); it('should allow shared album access', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(false); - accessMock.asset.hasPartnerAccess.mockResolvedValue(false); - accessMock.asset.hasAlbumAccess.mockResolvedValue(true); + accessMock.asset.checkAlbumAccess.mockResolvedValue(new Set([assetStub.image.id])); assetRepositoryMock.getById.mockResolvedValue(assetStub.image); await sut.getAssetById(authStub.admin, assetStub.image.id); - expect(accessMock.asset.hasAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, assetStub.image.id); + expect(accessMock.asset.checkAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, new Set([assetStub.image.id])); }); it('should throw an error for no access', async () => { - accessMock.asset.hasOwnerAccess.mockResolvedValue(false); - accessMock.asset.hasPartnerAccess.mockResolvedValue(false); - accessMock.asset.hasSharedLinkAccess.mockResolvedValue(false); - accessMock.asset.hasAlbumAccess.mockResolvedValue(false); await expect(sut.getAssetById(authStub.admin, assetStub.image.id)).rejects.toBeInstanceOf(BadRequestException); expect(assetRepositoryMock.getById).not.toHaveBeenCalled(); }); it('should throw an error for an invalid shared link', async () => { - accessMock.asset.hasSharedLinkAccess.mockResolvedValue(false); await expect(sut.getAssetById(authStub.adminSharedLink, assetStub.image.id)).rejects.toBeInstanceOf( BadRequestException, ); - expect(accessMock.asset.hasOwnerAccess).not.toHaveBeenCalled(); + expect(accessMock.asset.checkOwnerAccess).not.toHaveBeenCalled(); expect(assetRepositoryMock.getById).not.toHaveBeenCalled(); }); }); diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index f6886a255..48b64672d 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -4,10 +4,8 @@ import { AuthUserDto, getLivePhotoMotionFilename, IAccessRepository, - ICryptoRepository, IJobRepository, ILibraryRepository, - IStorageRepository, JobName, mapAsset, mimeTypes, @@ -16,28 +14,19 @@ import { UploadFile, } from '@app/domain'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, LibraryType } from '@app/infra/entities'; -import { - BadRequestException, - Inject, - Injectable, - InternalServerErrorException, - Logger, - NotFoundException, -} from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; import { Response as Res, Response } from 'express'; import { constants } from 'fs'; import fs from 'fs/promises'; import path from 'path'; -import { QueryFailedError, Repository } from 'typeorm'; +import { QueryFailedError } from 'typeorm'; import { IAssetRepository } from './asset-repository'; import { AssetCore } from './asset.core'; import { AssetBulkUploadCheckDto } from './dto/asset-check.dto'; import { AssetSearchDto } from './dto/asset-search.dto'; import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto'; -import { CreateAssetDto, ImportAssetDto } from './dto/create-asset.dto'; +import { CreateAssetDto } from './dto/create-asset.dto'; import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto'; -import { SearchAssetDto } from './dto/search-asset.dto'; import { SearchPropertiesDto } from './dto/search-properties.dto'; import { ServeFileDto } from './dto/serve-file.dto'; import { @@ -62,11 +51,8 @@ export class AssetService { constructor( @Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAssetRepository) private _assetRepository: IAssetRepository, - @InjectRepository(AssetEntity) private assetRepository: Repository, - @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ILibraryRepository) private libraryRepository: ILibraryRepository, - @Inject(IStorageRepository) private storageRepository: IStorageRepository, ) { this.assetCore = new AssetCore(_assetRepository, jobRepository); this.access = AccessCore.create(accessRepository); @@ -124,59 +110,6 @@ export class AssetService { } } - public async importFile(authUser: AuthUserDto, dto: ImportAssetDto): Promise { - dto = { - ...dto, - assetPath: path.resolve(dto.assetPath), - sidecarPath: dto.sidecarPath ? path.resolve(dto.sidecarPath) : undefined, - }; - - if (!mimeTypes.isAsset(dto.assetPath)) { - throw new BadRequestException(`Unsupported file type ${dto.assetPath}`); - } - - if (dto.sidecarPath && !mimeTypes.isSidecar(dto.sidecarPath)) { - throw new BadRequestException(`Unsupported sidecar file type`); - } - - for (const filepath of [dto.assetPath, dto.sidecarPath]) { - if (!filepath) { - continue; - } - - const exists = await this.storageRepository.checkFileExists(filepath, constants.R_OK); - if (!exists) { - throw new BadRequestException('File does not exist'); - } - } - - if (!authUser.externalPath || !dto.assetPath.match(new RegExp(`^${authUser.externalPath}`))) { - throw new BadRequestException("File does not exist within user's external path"); - } - - const assetFile: UploadFile = { - checksum: await this.cryptoRepository.hashFile(dto.assetPath), - originalPath: dto.assetPath, - originalName: path.parse(dto.assetPath).name, - }; - - try { - const libraryId = await this.getLibraryId(authUser, dto.libraryId); - await this.access.requirePermission(authUser, Permission.ASSET_UPLOAD, libraryId); - const asset = await this.assetCore.create(authUser, { ...dto, libraryId }, assetFile, undefined, dto.sidecarPath); - return { id: asset.id, duplicate: false }; - } catch (error: QueryFailedError | Error | any) { - // handle duplicates with a success response - if (error instanceof QueryFailedError && (error as any).constraint === ASSET_CHECKSUM_CONSTRAINT) { - const [duplicate] = await this._assetRepository.getAssetsByChecksums(authUser.id, [assetFile.checksum]); - return { id: duplicate.id, duplicate: true }; - } - - this.logger.error(`Error importing file ${error}`, error?.stack); - throw new BadRequestException(`Error importing file`, `${error}`); - } - } - public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) { return this._assetRepository.getAllByDeviceId(authUser.id, deviceId); } @@ -285,30 +218,6 @@ export class AssetService { return Array.from(possibleSearchTerm).filter((x) => x != null && x != ''); } - async searchAsset(authUser: AuthUserDto, searchAssetDto: SearchAssetDto): Promise { - const query = ` - SELECT a.* - FROM assets a - LEFT JOIN smart_info si ON a.id = si."assetId" - LEFT JOIN exif e ON a.id = e."assetId" - - WHERE a."ownerId" = $1 - AND - ( - TO_TSVECTOR('english', ARRAY_TO_STRING(si.tags, ',')) @@ PLAINTO_TSQUERY('english', $2) OR - TO_TSVECTOR('english', ARRAY_TO_STRING(si.objects, ',')) @@ PLAINTO_TSQUERY('english', $2) OR - e."exifTextSearchableColumn" @@ PLAINTO_TSQUERY('english', $2) - ); - `; - - const searchResults: AssetEntity[] = await this.assetRepository.query(query, [ - authUser.id, - searchAssetDto.searchTerm, - ]); - - return searchResults.map((asset) => mapAsset(asset)); - } - async getCuratedLocation(authUser: AuthUserDto): Promise { return this._assetRepository.getLocationsByUserId(authUser.id); } diff --git a/server/src/immich/api-v1/asset/dto/create-asset.dto.ts b/server/src/immich/api-v1/asset/dto/create-asset.dto.ts index 0338fe792..ae347e61b 100644 --- a/server/src/immich/api-v1/asset/dto/create-asset.dto.ts +++ b/server/src/immich/api-v1/asset/dto/create-asset.dto.ts @@ -70,23 +70,3 @@ export class CreateAssetDto extends CreateAssetBase { @ApiProperty({ type: 'string', format: 'binary', required: false }) [UploadFieldName.SIDECAR_DATA]?: any; } - -export class ImportAssetDto extends CreateAssetBase { - @Optional() - @IsBoolean() - @Transform(toBoolean) - isReadOnly?: boolean = true; - - @ValidateUUID() - @Optional() - libraryId?: string; - - @IsString() - @IsNotEmpty() - assetPath!: string; - - @IsString() - @Optional() - @IsNotEmpty() - sidecarPath?: string; -} diff --git a/server/src/immich/api-v1/asset/dto/device-id.dto.ts b/server/src/immich/api-v1/asset/dto/device-id.dto.ts index ff2f4163b..cae5f60c8 100644 --- a/server/src/immich/api-v1/asset/dto/device-id.dto.ts +++ b/server/src/immich/api-v1/asset/dto/device-id.dto.ts @@ -1,9 +1,7 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsUUID } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; export class DeviceIdDto { @IsNotEmpty() - @IsUUID('4') - @ApiProperty({ format: 'uuid' }) + @IsString() deviceId!: string; } diff --git a/server/src/immich/api-v1/asset/dto/search-asset.dto.ts b/server/src/immich/api-v1/asset/dto/search-asset.dto.ts deleted file mode 100644 index 83a34239d..000000000 --- a/server/src/immich/api-v1/asset/dto/search-asset.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsNotEmpty } from 'class-validator'; - -export class SearchAssetDto { - @IsNotEmpty() - searchTerm!: string; -} diff --git a/server/src/immich/app.guard.ts b/server/src/immich/app.guard.ts index bf4538ad0..0a9fe2dc1 100644 --- a/server/src/immich/app.guard.ts +++ b/server/src/immich/app.guard.ts @@ -20,16 +20,9 @@ export enum Metadata { PUBLIC_SECURITY = 'public_security', } -const adminDecorator = SetMetadata(Metadata.ADMIN_ROUTE, true); - -const sharedLinkDecorators = [ - SetMetadata(Metadata.SHARED_ROUTE, true), - ApiQuery({ name: 'key', type: String, required: false }), -]; - export interface AuthenticatedOptions { - admin?: boolean; - isShared?: boolean; + admin?: true; + isShared?: true; } export const Authenticated = (options: AuthenticatedOptions = {}) => { @@ -41,11 +34,11 @@ export const Authenticated = (options: AuthenticatedOptions = {}) => { ]; if (options.admin) { - decorators.push(adminDecorator); + decorators.push(AdminRoute()); } if (options.isShared) { - decorators.push(...sharedLinkDecorators); + decorators.push(SharedLinkRoute()); } return applyDecorators(...decorators); @@ -53,8 +46,9 @@ export const Authenticated = (options: AuthenticatedOptions = {}) => { export const PublicRoute = () => applyDecorators(SetMetadata(Metadata.AUTH_ROUTE, false), ApiSecurity(Metadata.PUBLIC_SECURITY)); -export const SharedLinkRoute = () => applyDecorators(...sharedLinkDecorators); -export const AdminRoute = () => adminDecorator; +export const SharedLinkRoute = () => + applyDecorators(SetMetadata(Metadata.SHARED_ROUTE, true), ApiQuery({ name: 'key', type: String, required: false })); +export const AdminRoute = (value = true) => SetMetadata(Metadata.ADMIN_ROUTE, value); export const AuthUser = createParamDecorator((data, ctx: ExecutionContext): AuthUserDto => { return ctx.switchToHttp().getRequest<{ user: AuthUserDto }>().user; diff --git a/server/src/immich/app.module.ts b/server/src/immich/app.module.ts index cf95eb236..7f9edfd42 100644 --- a/server/src/immich/app.module.ts +++ b/server/src/immich/app.module.ts @@ -16,6 +16,7 @@ import { AlbumController, AppController, AssetController, + AssetsController, AuditController, AuthController, JobController, @@ -41,6 +42,7 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors'; ], controllers: [ ActivityController, + AssetsController, AssetController, AssetControllerV1, AppController, diff --git a/server/src/immich/app.utils.ts b/server/src/immich/app.utils.ts index a7584cb1a..a667dce9f 100644 --- a/server/src/immich/app.utils.ts +++ b/server/src/immich/app.utils.ts @@ -13,6 +13,7 @@ import { SwaggerDocumentOptions, SwaggerModule, } from '@nestjs/swagger'; +import { NextFunction, Request, Response } from 'express'; import { writeFileSync } from 'fs'; import path from 'path'; @@ -56,6 +57,12 @@ const patchOpenAPI = (document: OpenAPIObject) => { document.components.schemas = sortKeys(document.components.schemas); } + for (const [key, value] of Object.entries(document.paths)) { + const newKey = key.replace('/api/', '/'); + delete document.paths[key]; + document.paths[newKey] = value; + } + for (const path of Object.values(document.paths)) { const operations = { get: path.get, @@ -94,6 +101,14 @@ const patchOpenAPI = (document: OpenAPIObject) => { return document; }; +export const indexFallback = (excludePaths: string[]) => (req: Request, res: Response, next: NextFunction) => { + if (req.url.startsWith('/api') || req.method.toLowerCase() !== 'get' || excludePaths.indexOf(req.url) !== -1) { + next(); + } else { + res.sendFile('/www/index.html', { root: process.cwd() }); + } +}; + export const useSwagger = (app: INestApplication, isDev: boolean) => { const config = new DocumentBuilder() .setTitle('Immich') diff --git a/server/src/immich/controllers/album.controller.ts b/server/src/immich/controllers/album.controller.ts index 7c2b39943..4216ca331 100644 --- a/server/src/immich/controllers/album.controller.ts +++ b/server/src/immich/controllers/album.controller.ts @@ -31,23 +31,31 @@ export class AlbumController { } @Get() - getAllAlbums(@AuthUser() authUser: AuthUserDto, @Query() query: GetAlbumsDto) { + getAllAlbums(@AuthUser() authUser: AuthUserDto, @Query() query: GetAlbumsDto): Promise { return this.service.getAll(authUser, query); } @Post() - createAlbum(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateDto) { + createAlbum(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateDto): Promise { return this.service.create(authUser, dto); } @SharedLinkRoute() @Get(':id') - getAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Query() dto: AlbumInfoDto) { + getAlbumInfo( + @AuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + @Query() dto: AlbumInfoDto, + ): Promise { return this.service.get(authUser, id, dto); } @Patch(':id') - updateAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto) { + updateAlbumInfo( + @AuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + @Body() dto: UpdateDto, + ): Promise { return this.service.update(authUser, id, dto); } diff --git a/server/src/immich/controllers/app.controller.ts b/server/src/immich/controllers/app.controller.ts index 9764ceb14..bd7c5d4c7 100644 --- a/server/src/immich/controllers/app.controller.ts +++ b/server/src/immich/controllers/app.controller.ts @@ -1,15 +1,34 @@ import { SystemConfigService } from '@app/domain'; -import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { Controller, Get, Header, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiExcludeEndpoint } from '@nestjs/swagger'; +import { PublicRoute } from '../app.guard'; @Controller() export class AppController { - constructor(private configService: SystemConfigService) {} + constructor(private service: SystemConfigService) {} + + @ApiExcludeEndpoint() + @Get('.well-known/immich') + getImmichWellKnown() { + return { + api: { + endpoint: '/api', + }, + }; + } + + @ApiExcludeEndpoint() + @PublicRoute() + @Get('custom.css') + @Header('Content-Type', 'text/css') + getCustomCss() { + return this.service.getCustomCss(); + } @ApiExcludeEndpoint() @Post('refresh-config') @HttpCode(HttpStatus.OK) public reloadConfig() { - return this.configService.refreshConfig(); + return this.service.refreshConfig(); } } diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/immich/controllers/asset.controller.ts index d0f294d1e..3a652c2e5 100644 --- a/server/src/immich/controllers/asset.controller.ts +++ b/server/src/immich/controllers/asset.controller.ts @@ -4,6 +4,7 @@ import { AssetIdsDto, AssetJobsDto, AssetResponseDto, + AssetSearchDto, AssetService, AssetStatsDto, AssetStatsResponseDto, @@ -37,11 +38,25 @@ import { StreamableFile, } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { DeviceIdDto } from '../api-v1/asset/dto/device-id.dto'; import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard'; import { UseValidation, asStreamableFile } from '../app.utils'; import { Route } from '../interceptors'; import { UUIDParamDto } from './dto/uuid-param.dto'; +@ApiTags('Asset') +@Controller('assets') +@Authenticated() +@UseValidation() +export class AssetsController { + constructor(private service: AssetService) {} + + @Get() + searchAssets(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetSearchDto): Promise { + return this.service.search(authUser, dto); + } +} + @ApiTags('Asset') @Controller(Route.ASSET) @Authenticated() @@ -86,6 +101,14 @@ export class AssetController { return this.service.downloadFile(authUser, id).then(asStreamableFile); } + /** + * Get all asset of a device that are in the database, ID only. + */ + @Get('/device/:deviceId') + getAllUserAssetsByDeviceId(@AuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) { + return this.service.getUserAssetsByDeviceId(authUser, deviceId); + } + @Get('statistics') getAssetStatistics(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetStatsDto): Promise { return this.service.getStatistics(authUser, dto); diff --git a/server/src/immich/controllers/auth.controller.ts b/server/src/immich/controllers/auth.controller.ts index 83e4b5145..dda546cf0 100644 --- a/server/src/immich/controllers/auth.controller.ts +++ b/server/src/immich/controllers/auth.controller.ts @@ -1,5 +1,4 @@ import { - AdminSignupResponseDto, AuthDeviceResponseDto, AuthService, AuthUserDto, @@ -13,9 +12,10 @@ import { SignUpDto, UserResponseDto, ValidateAccessTokenResponseDto, + mapUser, } from '@app/domain'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common'; -import { ApiBadRequestResponse, ApiTags } from '@nestjs/swagger'; +import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; import { AuthUser, Authenticated, GetLoginDetails, PublicRoute } from '../app.guard'; import { UseValidation } from '../app.utils'; @@ -42,9 +42,8 @@ export class AuthController { @PublicRoute() @Post('admin-sign-up') - @ApiBadRequestResponse({ description: 'The server already has an admin' }) - signUpAdmin(@Body() signUpCredential: SignUpDto): Promise { - return this.service.adminSignUp(signUpCredential); + signUpAdmin(@Body() dto: SignUpDto): Promise { + return this.service.adminSignUp(dto); } @Get('devices') @@ -73,7 +72,7 @@ export class AuthController { @Post('change-password') @HttpCode(HttpStatus.OK) changePassword(@AuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise { - return this.service.changePassword(authUser, dto); + return this.service.changePassword(authUser, dto).then(mapUser); } @Post('logout') diff --git a/server/src/immich/controllers/partner.controller.ts b/server/src/immich/controllers/partner.controller.ts index 0ecec54cd..5f9f004f9 100644 --- a/server/src/immich/controllers/partner.controller.ts +++ b/server/src/immich/controllers/partner.controller.ts @@ -1,5 +1,6 @@ -import { AuthUserDto, PartnerDirection, PartnerService, UserResponseDto } from '@app/domain'; -import { Controller, Delete, Get, Param, Post, Query } from '@nestjs/common'; +import { AuthUserDto, PartnerDirection, PartnerService } from '@app/domain'; +import { PartnerResponseDto, UpdatePartnerDto } from '@app/domain/partner/partner.dto'; +import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { ApiQuery, ApiTags } from '@nestjs/swagger'; import { AuthUser, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; @@ -17,15 +18,24 @@ export class PartnerController { getPartners( @AuthUser() authUser: AuthUserDto, @Query('direction') direction: PartnerDirection, - ): Promise { + ): Promise { return this.service.getAll(authUser, direction); } @Post(':id') - createPartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { + createPartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { return this.service.create(authUser, id); } + @Put(':id') + updatePartner( + @AuthUser() authUser: AuthUserDto, + @Param() { id }: UUIDParamDto, + @Body() dto: UpdatePartnerDto, + ): Promise { + return this.service.update(authUser, id, dto); + } + @Delete(':id') removePartner(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(authUser, id); diff --git a/server/src/immich/controllers/search.controller.ts b/server/src/immich/controllers/search.controller.ts index ffa454cd5..b3de4b26c 100644 --- a/server/src/immich/controllers/search.controller.ts +++ b/server/src/immich/controllers/search.controller.ts @@ -19,11 +19,6 @@ import { UseValidation } from '../app.utils'; export class SearchController { constructor(private service: SearchService) {} - @Get('person') - searchPerson(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchPeopleDto): Promise { - return this.service.searchPerson(authUser, dto); - } - @Get() search(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchDto): Promise { return this.service.search(authUser, dto); @@ -33,4 +28,9 @@ export class SearchController { getExploreData(@AuthUser() authUser: AuthUserDto): Promise { return this.service.getExploreData(authUser) as Promise; } + + @Get('person') + searchPerson(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchPeopleDto): Promise { + return this.service.searchPerson(authUser, dto); + } } diff --git a/server/src/immich/controllers/system-config.controller.ts b/server/src/immich/controllers/system-config.controller.ts index aa44c7acd..da2402efe 100644 --- a/server/src/immich/controllers/system-config.controller.ts +++ b/server/src/immich/controllers/system-config.controller.ts @@ -1,7 +1,8 @@ import { SystemConfigDto, SystemConfigService, SystemConfigTemplateStorageOptionDto } from '@app/domain'; -import { Body, Controller, Get, Put } from '@nestjs/common'; +import { MapThemeDto } from '@app/domain/system-config/system-config-map-theme.dto'; +import { Body, Controller, Get, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { Authenticated } from '../app.guard'; +import { AdminRoute, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; @ApiTags('System Config') @@ -30,4 +31,10 @@ export class SystemConfigController { getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { return this.service.getStorageTemplateOptions(); } + + @AdminRoute(false) + @Get('map/style.json') + getMapStyle(@Query() dto: MapThemeDto) { + return this.service.getMapStyle(dto.theme); + } } diff --git a/server/src/immich/controllers/user.controller.ts b/server/src/immich/controllers/user.controller.ts index 92b3fdcc0..1772fb548 100644 --- a/server/src/immich/controllers/user.controller.ts +++ b/server/src/immich/controllers/user.controller.ts @@ -13,6 +13,8 @@ import { Delete, Get, Header, + HttpCode, + HttpStatus, Param, Post, Put, @@ -54,6 +56,12 @@ export class UserController { return this.service.create(createUserDto); } + @Delete('profile-image') + @HttpCode(HttpStatus.NO_CONTENT) + deleteProfileImage(@AuthUser() authUser: AuthUserDto): Promise { + return this.service.deleteProfileImage(authUser); + } + @AdminRoute() @Delete(':id') deleteUser(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise { diff --git a/server/src/immich/main.ts b/server/src/immich/main.ts index e3ac81631..832d988af 100644 --- a/server/src/immich/main.ts +++ b/server/src/immich/main.ts @@ -6,7 +6,7 @@ import { NestExpressApplication } from '@nestjs/platform-express'; import { json } from 'body-parser'; import cookieParser from 'cookie-parser'; import { AppModule } from './app.module'; -import { useSwagger } from './app.utils'; +import { indexFallback, useSwagger } from './app.utils'; const logger = new Logger('ImmichServer'); const port = Number(process.env.SERVER_PORT) || 3001; @@ -24,6 +24,11 @@ export async function bootstrap() { app.useWebSocketAdapter(new RedisIoAdapter(app)); useSwagger(app, isDev); + const excludePaths = ['/.well-known/immich', '/custom.css']; + app.setGlobalPrefix('api', { exclude: excludePaths }); + app.useStaticAssets('www'); + app.use(indexFallback(excludePaths)); + const server = await app.listen(port); server.requestTimeout = 30 * 60 * 1000; diff --git a/server/src/infra/entities/album.entity.ts b/server/src/infra/entities/album.entity.ts index 38ce4310c..fbc125351 100644 --- a/server/src/infra/entities/album.entity.ts +++ b/server/src/infra/entities/album.entity.ts @@ -56,4 +56,7 @@ export class AlbumEntity { @OneToMany(() => SharedLinkEntity, (link) => link.album) sharedLinks!: SharedLinkEntity[]; + + @Column({ default: true }) + isActivityEnabled!: boolean; } diff --git a/server/src/infra/entities/asset-face.entity.ts b/server/src/infra/entities/asset-face.entity.ts index 66f5c2fd1..c47074d2e 100644 --- a/server/src/infra/entities/asset-face.entity.ts +++ b/server/src/infra/entities/asset-face.entity.ts @@ -1,8 +1,9 @@ -import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { AssetEntity } from './asset.entity'; import { PersonEntity } from './person.entity'; @Entity('asset_faces') +@Index(['personId', 'assetId']) export class AssetFaceEntity { @PrimaryGeneratedColumn('uuid') id!: string; diff --git a/server/src/infra/entities/asset-job-status.entity.ts b/server/src/infra/entities/asset-job-status.entity.ts new file mode 100644 index 000000000..36905cc8f --- /dev/null +++ b/server/src/infra/entities/asset-job-status.entity.ts @@ -0,0 +1,15 @@ +import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm'; +import { AssetEntity } from './asset.entity'; + +@Entity('asset_job_status') +export class AssetJobStatusEntity { + @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + @JoinColumn() + asset!: AssetEntity; + + @PrimaryColumn() + assetId!: string; + + @Column({ type: 'timestamptz', nullable: true }) + facesRecognizedAt!: Date | null; +} diff --git a/server/src/infra/entities/asset.entity.ts b/server/src/infra/entities/asset.entity.ts index 937107f9d..b1f254da4 100644 --- a/server/src/infra/entities/asset.entity.ts +++ b/server/src/infra/entities/asset.entity.ts @@ -15,6 +15,7 @@ import { } from 'typeorm'; import { AlbumEntity } from './album.entity'; import { AssetFaceEntity } from './asset-face.entity'; +import { AssetJobStatusEntity } from './asset-job-status.entity'; import { ExifEntity } from './exif.entity'; import { LibraryEntity } from './library.entity'; import { SharedLinkEntity } from './shared-link.entity'; @@ -32,6 +33,7 @@ export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_library_checksum'; @Index('IDX_day_of_month', { synchronize: false }) @Index('IDX_month', { synchronize: false }) @Index('IDX_originalPath_libraryId', ['originalPath', 'libraryId']) +@Index(['stackParentId']) // For all assets, each originalpath must be unique per user and library export class AssetEntity { @PrimaryGeneratedColumn('uuid') @@ -158,6 +160,9 @@ export class AssetEntity { @OneToMany(() => AssetEntity, (asset) => asset.stackParent) stack?: AssetEntity[]; + + @OneToOne(() => AssetJobStatusEntity, (jobStatus) => jobStatus.asset, { nullable: true }) + jobStatus?: AssetJobStatusEntity; } export enum AssetType { diff --git a/server/src/infra/entities/geodata-admin1.entity.ts b/server/src/infra/entities/geodata-admin1.entity.ts new file mode 100644 index 000000000..36cf0a805 --- /dev/null +++ b/server/src/infra/entities/geodata-admin1.entity.ts @@ -0,0 +1,10 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm'; + +@Entity('geodata_admin1') +export class GeodataAdmin1Entity { + @PrimaryColumn({ type: 'varchar' }) + key!: string; + + @Column({ type: 'varchar' }) + name!: string; +} diff --git a/server/src/infra/entities/geodata-admin2.entity.ts b/server/src/infra/entities/geodata-admin2.entity.ts new file mode 100644 index 000000000..bd03e8377 --- /dev/null +++ b/server/src/infra/entities/geodata-admin2.entity.ts @@ -0,0 +1,10 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm'; + +@Entity('geodata_admin2') +export class GeodataAdmin2Entity { + @PrimaryColumn({ type: 'varchar' }) + key!: string; + + @Column({ type: 'varchar' }) + name!: string; +} diff --git a/server/src/infra/entities/geodata-places.entity.ts b/server/src/infra/entities/geodata-places.entity.ts new file mode 100644 index 000000000..244e4261b --- /dev/null +++ b/server/src/infra/entities/geodata-places.entity.ts @@ -0,0 +1,59 @@ +import { GeodataAdmin1Entity } from '@app/infra/entities/geodata-admin1.entity'; +import { GeodataAdmin2Entity } from '@app/infra/entities/geodata-admin2.entity'; +import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm'; + +@Entity('geodata_places', { synchronize: false }) +export class GeodataPlacesEntity { + @PrimaryColumn({ type: 'integer' }) + id!: number; + + @Column({ type: 'varchar', length: 200 }) + name!: string; + + @Column({ type: 'float' }) + longitude!: number; + + @Column({ type: 'float' }) + latitude!: number; + + // @Column({ + // generatedType: 'STORED', + // asExpression: 'll_to_earth((latitude)::double precision, (longitude)::double precision)', + // type: 'earth', + // }) + earthCoord!: unknown; + + @Column({ type: 'char', length: 2 }) + countryCode!: string; + + @Column({ type: 'varchar', length: 20, nullable: true }) + admin1Code!: string; + + @Column({ type: 'varchar', length: 80, nullable: true }) + admin2Code!: string; + + @Column({ + type: 'varchar', + generatedType: 'STORED', + asExpression: `"countryCode" || '.' || "admin1Code"`, + nullable: true, + }) + admin1Key!: string; + + @ManyToOne(() => GeodataAdmin1Entity, { eager: true, nullable: true, createForeignKeyConstraints: false }) + admin1!: GeodataAdmin1Entity; + + @Column({ + type: 'varchar', + generatedType: 'STORED', + asExpression: `"countryCode" || '.' || "admin1Code" || '.' || "admin2Code"`, + nullable: true, + }) + admin2Key!: string; + + @ManyToOne(() => GeodataAdmin2Entity, { eager: true, nullable: true, createForeignKeyConstraints: false }) + admin2!: GeodataAdmin2Entity; + + @Column({ type: 'date' }) + modificationDate!: Date; +} diff --git a/server/src/infra/entities/index.ts b/server/src/infra/entities/index.ts index cbe2bf6c3..6c662a20a 100644 --- a/server/src/infra/entities/index.ts +++ b/server/src/infra/entities/index.ts @@ -1,10 +1,14 @@ +import { GeodataAdmin2Entity } from '@app/infra/entities/geodata-admin2.entity'; import { ActivityEntity } from './activity.entity'; import { AlbumEntity } from './album.entity'; import { APIKeyEntity } from './api-key.entity'; import { AssetFaceEntity } from './asset-face.entity'; +import { AssetJobStatusEntity } from './asset-job-status.entity'; import { AssetEntity } from './asset.entity'; import { AuditEntity } from './audit.entity'; import { ExifEntity } from './exif.entity'; +import { GeodataAdmin1Entity } from './geodata-admin1.entity'; +import { GeodataPlacesEntity } from './geodata-places.entity'; import { LibraryEntity } from './library.entity'; import { MoveEntity } from './move.entity'; import { PartnerEntity } from './partner.entity'; @@ -12,6 +16,7 @@ import { PersonEntity } from './person.entity'; import { SharedLinkEntity } from './shared-link.entity'; import { SmartInfoEntity } from './smart-info.entity'; import { SystemConfigEntity } from './system-config.entity'; +import { SystemMetadataEntity } from './system-metadata.entity'; import { TagEntity } from './tag.entity'; import { UserTokenEntity } from './user-token.entity'; import { UserEntity } from './user.entity'; @@ -20,9 +25,13 @@ export * from './activity.entity'; export * from './album.entity'; export * from './api-key.entity'; export * from './asset-face.entity'; +export * from './asset-job-status.entity'; export * from './asset.entity'; export * from './audit.entity'; export * from './exif.entity'; +export * from './geodata-admin1.entity'; +export * from './geodata-admin2.entity'; +export * from './geodata-places.entity'; export * from './library.entity'; export * from './move.entity'; export * from './partner.entity'; @@ -30,6 +39,7 @@ export * from './person.entity'; export * from './shared-link.entity'; export * from './smart-info.entity'; export * from './system-config.entity'; +export * from './system-metadata.entity'; export * from './tag.entity'; export * from './user-token.entity'; export * from './user.entity'; @@ -40,14 +50,19 @@ export const databaseEntities = [ APIKeyEntity, AssetEntity, AssetFaceEntity, + AssetJobStatusEntity, AuditEntity, ExifEntity, + GeodataPlacesEntity, + GeodataAdmin1Entity, + GeodataAdmin2Entity, MoveEntity, PartnerEntity, PersonEntity, SharedLinkEntity, SmartInfoEntity, SystemConfigEntity, + SystemMetadataEntity, TagEntity, UserEntity, UserTokenEntity, diff --git a/server/src/infra/entities/partner.entity.ts b/server/src/infra/entities/partner.entity.ts index d7be83082..35d32e4c9 100644 --- a/server/src/infra/entities/partner.entity.ts +++ b/server/src/infra/entities/partner.entity.ts @@ -1,4 +1,4 @@ -import { CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm'; +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm'; import { UserEntity } from './user.entity'; @@ -23,4 +23,7 @@ export class PartnerEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + + @Column({ type: 'boolean', default: false }) + inTimeline!: boolean; } diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index b71a44c0a..f6c14e1a7 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -62,10 +62,10 @@ export enum SystemConfigKey { MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES = 'machineLearning.facialRecognition.minFaces', MAP_ENABLED = 'map.enabled', - MAP_TILE_URL = 'map.tileUrl', + MAP_LIGHT_STYLE = 'map.lightStyle', + MAP_DARK_STYLE = 'map.darkStyle', REVERSE_GEOCODING_ENABLED = 'reverseGeocoding.enabled', - REVERSE_GEOCODING_CITIES_FILE_OVERRIDE = 'reverseGeocoding.citiesFileOverride', NEW_VERSION_CHECK_ENABLED = 'newVersionCheck.enabled', @@ -144,13 +144,6 @@ export enum Colorspace { P3 = 'p3', } -export enum CitiesFile { - CITIES_15000 = 'cities15000', - CITIES_5000 = 'cities5000', - CITIES_1000 = 'cities1000', - CITIES_500 = 'cities500', -} - export interface SystemConfig { ffmpeg: { crf: number; @@ -194,11 +187,11 @@ export interface SystemConfig { }; map: { enabled: boolean; - tileUrl: string; + lightStyle: string; + darkStyle: string; }; reverseGeocoding: { enabled: boolean; - citiesFileOverride: CitiesFile; }; oauth: { enabled: boolean; diff --git a/server/src/infra/entities/system-metadata.entity.ts b/server/src/infra/entities/system-metadata.entity.ts new file mode 100644 index 000000000..623806db7 --- /dev/null +++ b/server/src/infra/entities/system-metadata.entity.ts @@ -0,0 +1,18 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm'; + +@Entity('system_metadata') +export class SystemMetadataEntity { + @PrimaryColumn() + key!: string; + + @Column({ type: 'jsonb', default: '{}', transformer: { to: JSON.stringify, from: JSON.parse } }) + value!: { [key: string]: unknown }; +} + +export enum SystemMetadataKey { + REVERSE_GEOCODING_STATE = 'reverse-geocoding-state', +} + +export interface SystemMetadata extends Record { + [SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string }; +} diff --git a/server/src/infra/entities/user.entity.ts b/server/src/infra/entities/user.entity.ts index e6555153a..5a0a6afd6 100644 --- a/server/src/infra/entities/user.entity.ts +++ b/server/src/infra/entities/user.entity.ts @@ -10,16 +10,29 @@ import { import { AssetEntity } from './asset.entity'; import { TagEntity } from './tag.entity'; +export enum UserAvatarColor { + PRIMARY = 'primary', + PINK = 'pink', + RED = 'red', + YELLOW = 'yellow', + BLUE = 'blue', + GREEN = 'green', + PURPLE = 'purple', + ORANGE = 'orange', + GRAY = 'gray', + AMBER = 'amber', +} + @Entity('users') export class UserEntity { @PrimaryGeneratedColumn('uuid') id!: string; @Column({ default: '' }) - firstName!: string; + name!: string; - @Column({ default: '' }) - lastName!: string; + @Column({ type: 'varchar', nullable: true }) + avatarColor!: UserAvatarColor | null; @Column({ default: false }) isAdmin!: boolean; diff --git a/server/src/infra/infra.config.ts b/server/src/infra/infra.config.ts index 90477d8ca..7f2423032 100644 --- a/server/src/infra/infra.config.ts +++ b/server/src/infra/infra.config.ts @@ -74,6 +74,3 @@ function parseTypeSenseConfig(): ConfigurationOptions { } export const typesenseConfig: ConfigurationOptions = parseTypeSenseConfig(); - -export const REVERSE_GEOCODING_DUMP_DIRECTORY = - process.env.REVERSE_GEOCODING_DUMP_DIRECTORY || process.cwd() + '/.reverse-geocoding-dump/'; diff --git a/server/src/infra/infra.module.ts b/server/src/infra/infra.module.ts index 276058c0b..6cfaebefc 100644 --- a/server/src/infra/infra.module.ts +++ b/server/src/infra/infra.module.ts @@ -21,6 +21,7 @@ import { ISmartInfoRepository, IStorageRepository, ISystemConfigRepository, + ISystemMetadataRepository, ITagRepository, IUserRepository, IUserTokenRepository, @@ -35,10 +36,10 @@ import { databaseConfig } from './database.config'; import { databaseEntities } from './entities'; import { bullConfig, bullQueues } from './infra.config'; import { - APIKeyRepository, AccessRepository, ActivityRepository, AlbumRepository, + ApiKeyRepository, AssetRepository, AuditRepository, CommunicationRepository, @@ -56,6 +57,7 @@ import { SharedLinkRepository, SmartInfoRepository, SystemConfigRepository, + SystemMetadataRepository, TagRepository, TypesenseRepository, UserRepository, @@ -72,7 +74,7 @@ const providers: Provider[] = [ { provide: ICryptoRepository, useClass: CryptoRepository }, { provide: IJobRepository, useClass: JobRepository }, { provide: ILibraryRepository, useClass: LibraryRepository }, - { provide: IKeyRepository, useClass: APIKeyRepository }, + { provide: IKeyRepository, useClass: ApiKeyRepository }, { provide: IMachineLearningRepository, useClass: MachineLearningRepository }, { provide: IMetadataRepository, useClass: MetadataRepository }, { provide: IMoveRepository, useClass: MoveRepository }, @@ -84,6 +86,7 @@ const providers: Provider[] = [ { provide: ISmartInfoRepository, useClass: SmartInfoRepository }, { provide: IStorageRepository, useClass: FilesystemProvider }, { provide: ISystemConfigRepository, useClass: SystemConfigRepository }, + { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, { provide: ITagRepository, useClass: TagRepository }, { provide: IMediaRepository, useClass: MediaRepository }, { provide: IUserRepository, useClass: UserRepository }, diff --git a/server/src/infra/infra.util.ts b/server/src/infra/infra.util.ts new file mode 100644 index 000000000..8b2cd7efa --- /dev/null +++ b/server/src/infra/infra.util.ts @@ -0,0 +1,20 @@ +import { SetMetadata } from '@nestjs/common'; + +export const GENERATE_SQL_KEY = 'generate-sql-key'; + +export interface GenerateSqlQueries { + name?: string; + params?: any[]; +} + +/** Decorator to enable versioning/tracking of generated Sql */ +export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options); + +export const DummyValue = { + UUID: '00000000-0000-4000-a000-000000000000', + PAGINATION: { take: 10, skip: 0 }, + EMAIL: 'user@immich.app', + STRING: 'abcdefghi', + BUFFER: Buffer.from('abcdefghi'), + DATE: new Date(), +}; diff --git a/server/src/infra/migrations/1699268680508-DisableActivity.ts b/server/src/infra/migrations/1699268680508-DisableActivity.ts new file mode 100644 index 000000000..d860244f6 --- /dev/null +++ b/server/src/infra/migrations/1699268680508-DisableActivity.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class DisableActivity1699268680508 implements MigrationInterface { + name = 'DisableActivity1699268680508' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "albums" ADD "isActivityEnabled" boolean NOT NULL DEFAULT true`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "isActivityEnabled"`); + } + +} diff --git a/server/src/infra/migrations/1699322864544-UserNameConsolidation.ts b/server/src/infra/migrations/1699322864544-UserNameConsolidation.ts new file mode 100644 index 000000000..431a2a92f --- /dev/null +++ b/server/src/infra/migrations/1699322864544-UserNameConsolidation.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddUsername1699322864544 implements MigrationInterface { + name = 'AddUsername1699322864544' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" ADD "name" character varying NOT NULL DEFAULT ''`); + await queryRunner.query(`UPDATE "users" SET "name" = CONCAT(COALESCE("firstName", ''), ' ', COALESCE("lastName", ''))`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "firstName"`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "lastName"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "name"`); + await queryRunner.query(`ALTER TABLE "users" ADD "lastName" character varying NOT NULL DEFAULT ''`); + await queryRunner.query(`ALTER TABLE "users" ADD "firstName" character varying NOT NULL DEFAULT ''`); + await queryRunner.query(`UPDATE "users" SET "lastName" = COALESCE("email", '')`); + await queryRunner.query(`UPDATE "users" SET "firstName" = COALESCE("email", '')`); + } + +} diff --git a/server/src/infra/migrations/1699345863886-AddJobStatus.ts b/server/src/infra/migrations/1699345863886-AddJobStatus.ts new file mode 100644 index 000000000..c7df6387c --- /dev/null +++ b/server/src/infra/migrations/1699345863886-AddJobStatus.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddJobStatus1699345863886 implements MigrationInterface { + name = 'AddJobStatus1699345863886' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "asset_job_status" ("assetId" uuid NOT NULL, "facesRecognizedAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_420bec36fc02813bddf5c8b73d4" PRIMARY KEY ("assetId"))`); + await queryRunner.query(`ALTER TABLE "asset_job_status" ADD CONSTRAINT "FK_420bec36fc02813bddf5c8b73d4" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "asset_job_status" DROP CONSTRAINT "FK_420bec36fc02813bddf5c8b73d4"`); + await queryRunner.query(`DROP TABLE "asset_job_status"`); + } + +} diff --git a/server/src/infra/migrations/1699562570201-AdddInTimelineToPartnersTable.ts b/server/src/infra/migrations/1699562570201-AdddInTimelineToPartnersTable.ts new file mode 100644 index 000000000..59c2f229e --- /dev/null +++ b/server/src/infra/migrations/1699562570201-AdddInTimelineToPartnersTable.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AdddInTimelineToPartnersTable1699562570201 implements MigrationInterface { + name = 'AdddInTimelineToPartnersTable1699562570201' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "partners" ADD "inTimeline" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "partners" DROP COLUMN "inTimeline"`); + } + +} diff --git a/server/src/infra/migrations/1699889987493-AddAvatarColor.ts b/server/src/infra/migrations/1699889987493-AddAvatarColor.ts new file mode 100644 index 000000000..b075a5d2a --- /dev/null +++ b/server/src/infra/migrations/1699889987493-AddAvatarColor.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddAvatarColor1699889987493 implements MigrationInterface { + name = 'AddAvatarColor1699889987493' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" ADD "avatarColor" character varying`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "avatarColor"`); + } + +} diff --git a/server/src/infra/migrations/1700345818045-SystemMetadata.ts b/server/src/infra/migrations/1700345818045-SystemMetadata.ts new file mode 100644 index 000000000..0bd9162db --- /dev/null +++ b/server/src/infra/migrations/1700345818045-SystemMetadata.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class SystemMetadata1700345818045 implements MigrationInterface { + name = 'SystemMetadata1700345818045' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "system_metadata" ("key" character varying NOT NULL, "value" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_fa94f6857470fb5b81ec6084465" PRIMARY KEY ("key"))`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "system_metadata"`); + } + +} diff --git a/server/src/infra/migrations/1700362016675-Geodata.ts b/server/src/infra/migrations/1700362016675-Geodata.ts new file mode 100644 index 000000000..1ef562ff7 --- /dev/null +++ b/server/src/infra/migrations/1700362016675-Geodata.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Geodata1700362016675 implements MigrationInterface { + name = 'Geodata1700362016675' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS cube`) + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS earthdistance`) + await queryRunner.query(`CREATE TABLE "geodata_admin2" ("key" character varying NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_1e3886455dbb684d6f6b4756726" PRIMARY KEY ("key"))`); + await queryRunner.query(`CREATE TABLE "geodata_admin1" ("key" character varying NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_3fe3a89c5aac789d365871cb172" PRIMARY KEY ("key"))`); + await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`, ["immich","public","geodata_places","GENERATED_COLUMN","admin1Key","\"countryCode\" || '.' || \"admin1Code\""]); + await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`, ["immich","public","geodata_places","GENERATED_COLUMN","admin2Key","\"countryCode\" || '.' || \"admin1Code\" || '.' || \"admin2Code\""]); + await queryRunner.query(`CREATE TABLE "geodata_places" ("id" integer NOT NULL, "name" character varying(200) NOT NULL, "longitude" double precision NOT NULL, "latitude" double precision NOT NULL, "countryCode" character(2) NOT NULL, "admin1Code" character varying(20), "admin2Code" character varying(80), "admin1Key" character varying GENERATED ALWAYS AS ("countryCode" || '.' || "admin1Code") STORED, "admin2Key" character varying GENERATED ALWAYS AS ("countryCode" || '.' || "admin1Code" || '.' || "admin2Code") STORED, "modificationDate" date NOT NULL, CONSTRAINT "PK_c29918988912ef4036f3d7fbff4" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "geodata_places" ADD "earthCoord" earth GENERATED ALWAYS AS (ll_to_earth(latitude, longitude)) STORED`) + await queryRunner.query(`CREATE INDEX "IDX_geodata_gist_earthcoord" ON "geodata_places" USING gist ("earthCoord");`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_geodata_gist_earthcoord"`); + await queryRunner.query(`DROP TABLE "geodata_places"`); + await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","admin2Key","immich","public","geodata_places"]); + await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","admin1Key","immich","public","geodata_places"]); + await queryRunner.query(`DROP TABLE "geodata_admin1"`); + await queryRunner.query(`DROP TABLE "geodata_admin2"`); + await queryRunner.query(`DROP EXTENSION cube`); + await queryRunner.query(`DROP EXTENSION earthdistance`); + } + +} diff --git a/server/src/infra/migrations/1700752078178-AddAssetFaceIndicies.ts b/server/src/infra/migrations/1700752078178-AddAssetFaceIndicies.ts new file mode 100644 index 000000000..723b22b3d --- /dev/null +++ b/server/src/infra/migrations/1700752078178-AddAssetFaceIndicies.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddAssetFaceIndicies1700752078178 implements MigrationInterface { + name = 'AddAssetFaceIndicies1700752078178' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE INDEX "IDX_bf339a24070dac7e71304ec530" ON "asset_faces" ("personId", "assetId") `); + await queryRunner.query(`CREATE INDEX "IDX_b463c8edb01364bf2beba08ef1" ON "assets" ("stackParentId") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_b463c8edb01364bf2beba08ef1"`); + await queryRunner.query(`DROP INDEX "public"."IDX_bf339a24070dac7e71304ec530"`); + } + +} diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index 566514796..208b7095c 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -1,6 +1,6 @@ import { IAccessRepository } from '@app/domain'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Brackets, In, Repository } from 'typeorm'; import { ActivityEntity, AlbumEntity, @@ -43,192 +43,296 @@ export class AccessRepository implements IAccessRepository { }, }); }, - }; - library = { - hasOwnerAccess: (userId: string, libraryId: string): Promise => { - return this.libraryRepository.exist({ - where: { - id: libraryId, - ownerId: userId, - }, + hasCreateAccess: (userId: string, albumId: string): Promise => { + return this.albumRepository.exist({ + where: [ + { + id: albumId, + isActivityEnabled: true, + sharedUsers: { + id: userId, + }, + }, + { + id: albumId, + isActivityEnabled: true, + ownerId: userId, + }, + ], }); }, - hasPartnerAccess: (userId: string, partnerId: string): Promise => { - return this.partnerRepository.exist({ - where: { - sharedWithId: userId, - sharedById: partnerId, - }, - }); + }; + + library = { + checkOwnerAccess: async (userId: string, libraryIds: Set): Promise> => { + if (libraryIds.size === 0) { + return new Set(); + } + + return this.libraryRepository + .find({ + select: { id: true }, + where: { + id: In([...libraryIds]), + ownerId: userId, + }, + }) + .then((libraries) => new Set(libraries.map((library) => library.id))); + }, + + checkPartnerAccess: async (userId: string, partnerIds: Set): Promise> => { + if (partnerIds.size === 0) { + return new Set(); + } + + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); }, }; timeline = { - hasPartnerAccess: (userId: string, partnerId: string): Promise => { - return this.partnerRepository.exist({ - where: { - sharedWithId: userId, - sharedById: partnerId, - }, - }); + checkPartnerAccess: async (userId: string, partnerIds: Set): Promise> => { + if (partnerIds.size === 0) { + return new Set(); + } + + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); }, }; asset = { - hasAlbumAccess: (userId: string, assetId: string): Promise => { - return this.albumRepository.exist({ - where: [ - { + checkAlbumAccess: async (userId: string, assetIds: Set): Promise> => { + if (assetIds.size === 0) { + return new Set(); + } + + return this.albumRepository + .createQueryBuilder('album') + .innerJoin('album.assets', 'asset') + .leftJoin('album.sharedUsers', 'sharedUsers') + .select('asset.id', 'assetId') + .addSelect('asset.livePhotoVideoId', 'livePhotoVideoId') + .where( + new Brackets((qb) => { + qb.where('album.ownerId = :userId', { userId }).orWhere('sharedUsers.id = :userId', { userId }); + }), + ) + .andWhere( + new Brackets((qb) => { + qb.where('asset.id IN (:...assetIds)', { assetIds: [...assetIds] }) + // still part of a live photo is in an album + .orWhere('asset.livePhotoVideoId IN (:...assetIds)', { assetIds: [...assetIds] }); + }), + ) + .getRawMany() + .then((rows) => { + const allowedIds = new Set(); + for (const row of rows) { + if (row.assetId && assetIds.has(row.assetId)) { + allowedIds.add(row.assetId); + } + if (row.livePhotoVideoId && assetIds.has(row.livePhotoVideoId)) { + allowedIds.add(row.livePhotoVideoId); + } + } + return allowedIds; + }); + }, + + checkOwnerAccess: async (userId: string, assetIds: Set): Promise> => { + if (assetIds.size === 0) { + return new Set(); + } + + return this.assetRepository + .find({ + select: { id: true }, + where: { + id: In([...assetIds]), ownerId: userId, - assets: { - id: assetId, - }, }, - { - sharedUsers: { - id: userId, - }, - assets: { - id: assetId, - }, - }, - // still part of a live photo is in an album - { - ownerId: userId, - assets: { - livePhotoVideoId: assetId, - }, - }, - { - sharedUsers: { - id: userId, - }, - assets: { - livePhotoVideoId: assetId, - }, - }, - ], - }); + withDeleted: true, + }) + .then((assets) => new Set(assets.map((asset) => asset.id))); }, - hasOwnerAccess: (userId: string, assetId: string): Promise => { - return this.assetRepository.exist({ - where: { - id: assetId, - ownerId: userId, - }, - withDeleted: true, - }); + checkPartnerAccess: async (userId: string, assetIds: Set): Promise> => { + if (assetIds.size === 0) { + return new Set(); + } + + return this.partnerRepository + .createQueryBuilder('partner') + .innerJoin('partner.sharedBy', 'sharedBy') + .innerJoin('sharedBy.assets', 'asset') + .select('asset.id', 'assetId') + .where('partner.sharedWithId = :userId', { userId }) + .andWhere('asset.id IN (:...assetIds)', { assetIds: [...assetIds] }) + .getRawMany() + .then((rows) => new Set(rows.map((row) => row.assetId))); }, - hasPartnerAccess: (userId: string, assetId: string): Promise => { - return this.partnerRepository.exist({ - where: { - sharedWith: { - id: userId, - }, - sharedBy: { - assets: { - id: assetId, - }, - }, - }, - relations: { - sharedWith: true, - sharedBy: { - assets: true, - }, - }, - }); - }, + checkSharedLinkAccess: async (sharedLinkId: string, assetIds: Set): Promise> => { + if (assetIds.size === 0) { + return new Set(); + } - hasSharedLinkAccess: async (sharedLinkId: string, assetId: string): Promise => { - return this.sharedLinkRepository.exist({ - where: [ - { - id: sharedLinkId, - album: { - assets: { - id: assetId, - }, - }, - }, - { - id: sharedLinkId, - assets: { - id: assetId, - }, - }, - // still part of a live photo is in a shared link - { - id: sharedLinkId, - album: { - assets: { - livePhotoVideoId: assetId, - }, - }, - }, - { - id: sharedLinkId, - assets: { - livePhotoVideoId: assetId, - }, - }, - ], - }); + return this.sharedLinkRepository + .createQueryBuilder('sharedLink') + .leftJoin('sharedLink.album', 'album') + .leftJoin('sharedLink.assets', 'assets') + .leftJoin('album.assets', 'albumAssets') + .select('assets.id', 'assetId') + .addSelect('albumAssets.id', 'albumAssetId') + .addSelect('assets.livePhotoVideoId', 'assetLivePhotoVideoId') + .addSelect('albumAssets.livePhotoVideoId', 'albumAssetLivePhotoVideoId') + .where('sharedLink.id = :sharedLinkId', { sharedLinkId }) + .andWhere( + new Brackets((qb) => { + qb.where('assets.id IN (:...assetIds)', { assetIds: [...assetIds] }) + .orWhere('albumAssets.id IN (:...assetIds)', { assetIds: [...assetIds] }) + // still part of a live photo is in a shared link + .orWhere('assets.livePhotoVideoId IN (:...assetIds)', { assetIds: [...assetIds] }) + .orWhere('albumAssets.livePhotoVideoId IN (:...assetIds)', { assetIds: [...assetIds] }); + }), + ) + .getRawMany() + .then((rows) => { + const allowedIds = new Set(); + for (const row of rows) { + if (row.assetId && assetIds.has(row.assetId)) { + allowedIds.add(row.assetId); + } + if (row.assetLivePhotoVideoId && assetIds.has(row.assetLivePhotoVideoId)) { + allowedIds.add(row.assetLivePhotoVideoId); + } + if (row.albumAssetId && assetIds.has(row.albumAssetId)) { + allowedIds.add(row.albumAssetId); + } + if (row.albumAssetLivePhotoVideoId && assetIds.has(row.albumAssetLivePhotoVideoId)) { + allowedIds.add(row.albumAssetLivePhotoVideoId); + } + } + return allowedIds; + }); }, }; authDevice = { - hasOwnerAccess: (userId: string, deviceId: string): Promise => { - return this.tokenRepository.exist({ - where: { - userId, - id: deviceId, - }, - }); + checkOwnerAccess: async (userId: string, deviceIds: Set): Promise> => { + if (deviceIds.size === 0) { + return new Set(); + } + + return this.tokenRepository + .find({ + select: { id: true }, + where: { + userId, + id: In([...deviceIds]), + }, + }) + .then((tokens) => new Set(tokens.map((token) => token.id))); }, }; album = { - hasOwnerAccess: (userId: string, albumId: string): Promise => { - return this.albumRepository.exist({ - where: { - id: albumId, - ownerId: userId, - }, - }); - }, + checkOwnerAccess: async (userId: string, albumIds: Set): Promise> => { + if (albumIds.size === 0) { + return new Set(); + } - hasSharedAlbumAccess: (userId: string, albumId: string): Promise => { - return this.albumRepository.exist({ - where: { - id: albumId, - sharedUsers: { - id: userId, + return this.albumRepository + .find({ + select: { id: true }, + where: { + id: In([...albumIds]), + ownerId: userId, }, - }, - }); + }) + .then((albums) => new Set(albums.map((album) => album.id))); }, - hasSharedLinkAccess: (sharedLinkId: string, albumId: string): Promise => { - return this.sharedLinkRepository.exist({ - where: { - id: sharedLinkId, - albumId, - }, - }); + checkSharedAlbumAccess: async (userId: string, albumIds: Set): Promise> => { + if (albumIds.size === 0) { + return new Set(); + } + + return this.albumRepository + .find({ + select: { id: true }, + where: { + id: In([...albumIds]), + sharedUsers: { + id: userId, + }, + }, + }) + .then((albums) => new Set(albums.map((album) => album.id))); + }, + + checkSharedLinkAccess: async (sharedLinkId: string, albumIds: Set): Promise> => { + if (albumIds.size === 0) { + return new Set(); + } + + return this.sharedLinkRepository + .find({ + select: { albumId: true }, + where: { + id: sharedLinkId, + albumId: In([...albumIds]), + }, + }) + .then( + (sharedLinks) => + new Set(sharedLinks.flatMap((sharedLink) => (!!sharedLink.albumId ? [sharedLink.albumId] : []))), + ); }, }; person = { - hasOwnerAccess: (userId: string, personId: string): Promise => { - return this.personRepository.exist({ - where: { - id: personId, - ownerId: userId, - }, - }); + checkOwnerAccess: async (userId: string, personIds: Set): Promise> => { + if (personIds.size === 0) { + return new Set(); + } + + return this.personRepository + .find({ + select: { id: true }, + where: { + id: In([...personIds]), + ownerId: userId, + }, + }) + .then((persons) => new Set(persons.map((person) => person.id))); + }, + }; + + partner = { + checkUpdateAccess: async (userId: string, partnerIds: Set): Promise> => { + if (partnerIds.size === 0) { + return new Set(); + } + + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); }, }; } diff --git a/server/src/infra/repositories/activity.repository.ts b/server/src/infra/repositories/activity.repository.ts index 138d96381..319c9c647 100644 --- a/server/src/infra/repositories/activity.repository.ts +++ b/server/src/infra/repositories/activity.repository.ts @@ -3,25 +3,26 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { IsNull, Repository } from 'typeorm'; import { ActivityEntity } from '../entities/activity.entity'; +import { DummyValue, GenerateSql } from '../infra.util'; export interface ActivitySearch { albumId?: string; - assetId?: string; + assetId?: string | null; userId?: string; isLiked?: boolean; - isGlobal?: boolean; } @Injectable() export class ActivityRepository implements IActivityRepository { constructor(@InjectRepository(ActivityEntity) private repository: Repository) {} + @GenerateSql({ params: [{ albumId: DummyValue.UUID }] }) search(options: ActivitySearch): Promise { - const { userId, assetId, albumId, isLiked, isGlobal } = options; + const { userId, assetId, albumId, isLiked } = options; return this.repository.find({ where: { userId, - assetId: isGlobal ? IsNull() : assetId, + assetId: assetId === null ? IsNull() : assetId, albumId, isLiked, }, @@ -42,6 +43,7 @@ export class ActivityRepository implements IActivityRepository { await this.repository.delete(id); } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) getStatistics(assetId: string, albumId: string): Promise { return this.repository.count({ where: { assetId, albumId, isLiked: false }, diff --git a/server/src/infra/repositories/album.repository.ts b/server/src/infra/repositories/album.repository.ts index 69df22685..8a63897ed 100644 --- a/server/src/infra/repositories/album.repository.ts +++ b/server/src/infra/repositories/album.repository.ts @@ -4,6 +4,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm'; import { dataSource } from '../database.config'; import { AlbumEntity, AssetEntity } from '../entities'; +import { DummyValue, GenerateSql } from '../infra.util'; @Injectable() export class AlbumRepository implements IAlbumRepository { @@ -13,6 +14,7 @@ export class AlbumRepository implements IAlbumRepository { @InjectDataSource() private dataSource: DataSource, ) {} + @GenerateSql({ params: [DummyValue.UUID, {}] }) getById(id: string, options: AlbumInfoOptions): Promise { const relations: FindOptionsRelations = { owner: true, @@ -36,6 +38,7 @@ export class AlbumRepository implements IAlbumRepository { return this.repository.findOne({ where: { id }, relations, order }); } + @GenerateSql({ params: [[DummyValue.UUID]] }) getByIds(ids: string[]): Promise { return this.repository.find({ where: { @@ -48,6 +51,7 @@ export class AlbumRepository implements IAlbumRepository { }); } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) getByAssetId(ownerId: string, assetId: string): Promise { return this.repository.find({ where: [ @@ -59,25 +63,31 @@ export class AlbumRepository implements IAlbumRepository { }); } - async getAssetCountForIds(ids: string[]): Promise { + @GenerateSql({ params: [[DummyValue.UUID]] }) + async getMetadataForIds(ids: string[]): Promise { // Guard against running invalid query when ids list is empty. if (!ids.length) { return []; } // Only possible with query builder because of GROUP BY. - const countByAlbums = await this.repository + const albumMetadatas = await this.repository .createQueryBuilder('album') .select('album.id') - .addSelect('COUNT(albums_assets.assetsId)', 'asset_count') - .leftJoin('albums_assets_assets', 'albums_assets', 'albums_assets.albumsId = album.id') + .addSelect('MIN(assets.fileCreatedAt)', 'start_date') + .addSelect('MAX(assets.fileCreatedAt)', 'end_date') + .addSelect('COUNT(album_assets.assetsId)', 'asset_count') + .leftJoin('albums_assets_assets', 'album_assets', 'album_assets.albumsId = album.id') + .leftJoin('assets', 'assets', 'assets.id = album_assets.assetsId') .where('album.id IN (:...ids)', { ids }) .groupBy('album.id') .getRawMany(); - return countByAlbums.map((albumCount) => ({ - albumId: albumCount['album_id'], - assetCount: Number(albumCount['asset_count']), + return albumMetadatas.map((metadatas) => ({ + albumId: metadatas['album_id'], + assetCount: Number(metadatas['asset_count']), + startDate: metadatas['end_date'] ? new Date(metadatas['start_date']) : undefined, + endDate: metadatas['end_date'] ? new Date(metadatas['end_date']) : undefined, })); } @@ -86,6 +96,7 @@ export class AlbumRepository implements IAlbumRepository { * - Thumbnail references an asset outside the album * - Empty album still has a thumbnail set */ + @GenerateSql() async getInvalidThumbnail(): Promise { // Using dataSource, because there is no direct access to albums_assets_assets. const albumHasAssets = this.dataSource @@ -108,6 +119,7 @@ export class AlbumRepository implements IAlbumRepository { return albums.map((album) => album.id); } + @GenerateSql({ params: [DummyValue.UUID] }) getOwned(ownerId: string): Promise { return this.repository.find({ relations: { sharedUsers: true, sharedLinks: true, owner: true }, @@ -119,6 +131,7 @@ export class AlbumRepository implements IAlbumRepository { /** * Get albums shared with and shared by owner. */ + @GenerateSql({ params: [DummyValue.UUID] }) getShared(ownerId: string): Promise { return this.repository.find({ relations: { sharedUsers: true, sharedLinks: true, owner: true }, @@ -134,6 +147,7 @@ export class AlbumRepository implements IAlbumRepository { /** * Get albums of owner that are _not_ shared */ + @GenerateSql({ params: [DummyValue.UUID] }) getNotShared(ownerId: string): Promise { return this.repository.find({ relations: { sharedUsers: true, sharedLinks: true, owner: true }, @@ -154,6 +168,7 @@ export class AlbumRepository implements IAlbumRepository { await this.repository.delete({ ownerId: userId }); } + @GenerateSql() getAll(): Promise { return this.repository.find({ relations: { @@ -162,6 +177,7 @@ export class AlbumRepository implements IAlbumRepository { }); } + // @GenerateSql({ params: [DummyValue.UUID] }) async removeAsset(assetId: string): Promise { // Using dataSource, because there is no direct access to albums_assets_assets. await this.dataSource @@ -171,6 +187,7 @@ export class AlbumRepository implements IAlbumRepository { .where('"albums_assets_assets"."assetsId" = :assetId', { assetId }); } + @GenerateSql({ params: [{ albumId: DummyValue.UUID, assetIds: [DummyValue.UUID] }] }) async removeAssets(asset: AlbumAssets): Promise { await this.dataSource .createQueryBuilder() @@ -190,6 +207,7 @@ export class AlbumRepository implements IAlbumRepository { * @param assetIds Optional list of asset IDs to filter on. * @returns Set of Asset IDs for the given album ID. */ + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }, { name: 'no assets', params: [DummyValue.UUID] }) async getAssetIds(albumId: string, assetIds?: string[]): Promise> { const query = this.dataSource .createQueryBuilder() @@ -205,6 +223,7 @@ export class AlbumRepository implements IAlbumRepository { return new Set(result.map((row) => row['assetId'])); } + @GenerateSql({ params: [{ albumId: DummyValue.UUID, assetId: DummyValue.UUID }] }) hasAsset(asset: AlbumAsset): Promise { return this.repository.exist({ where: { @@ -219,6 +238,7 @@ export class AlbumRepository implements IAlbumRepository { }); } + @GenerateSql({ params: [{ albumId: DummyValue.UUID, assetIds: [DummyValue.UUID] }] }) async addAssets({ albumId, assetIds }: AlbumAssets): Promise { await this.dataSource .createQueryBuilder() @@ -261,6 +281,7 @@ export class AlbumRepository implements IAlbumRepository { * * @returns Amount of updated album thumbnails or undefined when unknown */ + @GenerateSql() async updateThumbnails(): Promise { // Subquery for getting a new thumbnail. const newThumbnail = this.assetRepository diff --git a/server/src/infra/repositories/api-key.repository.ts b/server/src/infra/repositories/api-key.repository.ts index 2484b0d56..71226a537 100644 --- a/server/src/infra/repositories/api-key.repository.ts +++ b/server/src/infra/repositories/api-key.repository.ts @@ -3,9 +3,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { APIKeyEntity } from '../entities'; +import { DummyValue, GenerateSql } from '../infra.util'; @Injectable() -export class APIKeyRepository implements IKeyRepository { +export class ApiKeyRepository implements IKeyRepository { constructor(@InjectRepository(APIKeyEntity) private repository: Repository) {} async create(dto: Partial): Promise { @@ -21,6 +22,7 @@ export class APIKeyRepository implements IKeyRepository { await this.repository.delete({ userId, id }); } + @GenerateSql({ params: [DummyValue.STRING] }) getKey(hashedToken: string): Promise { return this.repository.findOne({ select: { @@ -35,10 +37,12 @@ export class APIKeyRepository implements IKeyRepository { }); } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) getById(userId: string, id: string): Promise { return this.repository.findOne({ where: { userId, id } }); } + @GenerateSql({ params: [DummyValue.STRING] }) getByUserId(userId: string): Promise { return this.repository.find({ where: { userId }, order: { createdAt: 'DESC' } }); } diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index 199842ed4..09ecf6692 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -18,12 +18,16 @@ import { } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import _ from 'lodash'; import { DateTime } from 'luxon'; import { And, FindOptionsRelations, FindOptionsWhere, In, IsNull, LessThan, Not, Repository } from 'typeorm'; -import { AssetEntity, AssetType, ExifEntity } from '../entities'; +import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '../entities'; +import { DummyValue, GenerateSql } from '../infra.util'; import OptionalBetween from '../utils/optional-between.util'; import { paginate } from '../utils/pagination.util'; +const DEFAULT_SEARCH_SIZE = 250; + const truncateMap: Record = { [TimeBucketSize.DAY]: 'day', [TimeBucketSize.MONTH]: 'month', @@ -39,16 +43,150 @@ export class AssetRepository implements IAssetRepository { constructor( @InjectRepository(AssetEntity) private repository: Repository, @InjectRepository(ExifEntity) private exifRepository: Repository, + @InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository, ) {} async upsertExif(exif: Partial): Promise { await this.exifRepository.upsert(exif, { conflictPaths: ['assetId'] }); } + async upsertJobStatus(jobStatus: Partial): Promise { + await this.jobStatusRepository.upsert(jobStatus, { conflictPaths: ['assetId'] }); + } + + search(options: AssetSearchOptions): Promise { + const { + id, + libraryId, + deviceAssetId, + type, + checksum, + ownerId, + + isVisible, + isFavorite, + isExternal, + isReadOnly, + isOffline, + isArchived, + isMotion, + isEncoded, + + createdBefore, + createdAfter, + updatedBefore, + updatedAfter, + trashedBefore, + trashedAfter, + takenBefore, + takenAfter, + + originalFileName, + originalPath, + resizePath, + webpPath, + encodedVideoPath, + + city, + state, + country, + make, + model, + lensModel, + + withDeleted: _withDeleted, + withExif: _withExif, + withStacked, + withPeople, + + order, + } = options; + + const withDeleted = _withDeleted ?? (trashedAfter !== undefined || trashedBefore !== undefined); + + const page = Math.max(options.page || 1, 1); + const size = Math.min(options.size || DEFAULT_SEARCH_SIZE, DEFAULT_SEARCH_SIZE); + + const exifWhere = _.omitBy( + { + city, + state, + country, + make, + model, + lensModel, + }, + _.isUndefined, + ); + + const withExif = Object.keys(exifWhere).length > 0 || _withExif; + + const where = _.omitBy( + { + ownerId, + id, + libraryId, + deviceAssetId, + type, + checksum, + isVisible, + isFavorite, + isExternal, + isReadOnly, + isOffline, + isArchived, + livePhotoVideoId: isMotion && Not(IsNull()), + originalFileName, + originalPath, + resizePath, + webpPath, + encodedVideoPath: encodedVideoPath ?? (isEncoded && Not(IsNull())), + createdAt: OptionalBetween(createdAfter, createdBefore), + updatedAt: OptionalBetween(updatedAfter, updatedBefore), + deletedAt: OptionalBetween(trashedAfter, trashedBefore), + fileCreatedAt: OptionalBetween(takenAfter, takenBefore), + exifInfo: Object.keys(exifWhere).length > 0 ? exifWhere : undefined, + }, + _.isUndefined, + ); + + const builder = this.repository.createQueryBuilder('asset'); + + if (withExif) { + if (_withExif) { + builder.leftJoinAndSelect('asset.exifInfo', 'exifInfo'); + } else { + builder.leftJoin('asset.exifInfo', 'exifInfo'); + } + } + + if (withPeople) { + builder.leftJoinAndSelect('asset.faces', 'faces'); + builder.leftJoinAndSelect('faces.person', 'person'); + } + + if (withStacked) { + builder.leftJoinAndSelect('asset.stack', 'stack'); + } + + if (withDeleted) { + builder.withDeleted(); + } + + builder + .where(where) + .skip(size * (page - 1)) + .take(size) + .orderBy('asset.fileCreatedAt', order ?? 'DESC'); + + return builder.getMany(); + } + create(asset: AssetCreate): Promise { return this.repository.save(asset); } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.DATE] }) getByDate(ownerId: string, date: Date): Promise { // For reference of a correct approach although slower @@ -83,6 +221,7 @@ export class AssetRepository implements IAssetRepository { }); } + @GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] }) getByDayOfYear(ownerId: string, { day, month }: MonthDay): Promise { return this.repository .createQueryBuilder('entity') @@ -104,6 +243,7 @@ export class AssetRepository implements IAssetRepository { .getMany(); } + @GenerateSql({ params: [[DummyValue.UUID]] }) getByIds(ids: string[], relations?: FindOptionsRelations): Promise { if (!relations) { relations = { @@ -123,6 +263,7 @@ export class AssetRepository implements IAssetRepository { }); } + @GenerateSql({ params: [DummyValue.UUID] }) async deleteAll(ownerId: string): Promise { await this.repository.delete({ ownerId }); } @@ -155,12 +296,14 @@ export class AssetRepository implements IAssetRepository { }); } + @GenerateSql({ params: [[DummyValue.UUID]] }) getByLibraryId(libraryIds: string[]): Promise { return this.repository.find({ where: { library: { id: In(libraryIds) } }, }); } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise { return this.repository.findOne({ where: { library: { id: libraryId }, originalPath: originalPath }, @@ -190,6 +333,29 @@ export class AssetRepository implements IAssetRepository { }); } + /** + * Get assets by device's Id on the database + * @param ownerId + * @param deviceId + * + * @returns Promise - Array of assetIds belong to the device + */ + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) + async getAllByDeviceId(ownerId: string, deviceId: string): Promise { + const items = await this.repository.find({ + select: { deviceAssetId: true }, + where: { + ownerId, + deviceId, + isVisible: true, + }, + withDeleted: true, + }); + + return items.map((asset) => asset.deviceAssetId); + } + + @GenerateSql({ params: [DummyValue.UUID] }) getById(id: string): Promise { return this.repository.findOne({ where: { id }, @@ -205,6 +371,7 @@ export class AssetRepository implements IAssetRepository { }); } + @GenerateSql({ params: [[DummyValue.UUID], { deviceId: DummyValue.STRING }] }) async updateAll(ids: string[], options: Partial): Promise { await this.repository.update({ id: In(ids) }, options); } @@ -238,6 +405,7 @@ export class AssetRepository implements IAssetRepository { await this.repository.remove(asset); } + @GenerateSql({ params: [[DummyValue.UUID], DummyValue.BUFFER] }) getByChecksum(userId: string, checksum: Buffer): Promise { return this.repository.findOne({ where: { ownerId: userId, checksum } }); } @@ -260,6 +428,14 @@ export class AssetRepository implements IAssetRepository { }); } + @GenerateSql( + ...Object.values(WithProperty) + .filter((property) => property !== WithProperty.IS_OFFLINE) + .map((property) => ({ + name: property, + params: [DummyValue.PAGINATION, property], + })), + ) getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated { let relations: FindOptionsRelations = {}; let where: FindOptionsWhere | FindOptionsWhere[] = {}; @@ -323,6 +499,7 @@ export class AssetRepository implements IAssetRepository { case WithoutProperty.FACES: relations = { faces: true, + jobStatus: true, }; where = { resizePath: Not(IsNull()), @@ -331,6 +508,9 @@ export class AssetRepository implements IAssetRepository { assetId: IsNull(), personId: IsNull(), }, + jobStatus: { + facesRecognizedAt: IsNull(), + }, }; break; @@ -510,7 +690,7 @@ export class AssetRepository implements IAssetRepository { } private getBuilder(options: TimeBucketOptions) { - const { isArchived, isFavorite, isTrashed, albumId, personId, userId, withStacked } = options; + const { isArchived, isFavorite, isTrashed, albumId, personId, userIds, withStacked } = options; let builder = this.repository .createQueryBuilder('asset') @@ -523,11 +703,11 @@ export class AssetRepository implements IAssetRepository { builder = builder.leftJoin('asset.albums', 'album').andWhere('album.id = :albumId', { albumId }); } - if (userId) { - builder = builder.andWhere('asset.ownerId = :userId', { userId }); + if (userIds) { + builder = builder.andWhere('asset.ownerId IN (:...userIds )', { userIds }); } - if (isArchived != undefined) { + if (isArchived !== undefined) { builder = builder.andWhere('asset.isArchived = :isArchived', { isArchived }); } diff --git a/server/src/infra/repositories/communication.repository.ts b/server/src/infra/repositories/communication.repository.ts index e1bbc77de..3eabaa86a 100644 --- a/server/src/infra/repositories/communication.repository.ts +++ b/server/src/infra/repositories/communication.repository.ts @@ -3,7 +3,7 @@ import { Logger } from '@nestjs/common'; import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; -@WebSocketGateway({ cors: true }) +@WebSocketGateway({ cors: true, path: '/api/socket.io' }) export class CommunicationRepository implements OnGatewayConnection, OnGatewayDisconnect, ICommunicationRepository { private logger = new Logger(CommunicationRepository.name); private onConnectCallbacks: Callback[] = []; diff --git a/server/src/infra/repositories/index.ts b/server/src/infra/repositories/index.ts index 81ea7dd81..0324fef43 100644 --- a/server/src/infra/repositories/index.ts +++ b/server/src/infra/repositories/index.ts @@ -19,6 +19,7 @@ export * from './server-info.repository'; export * from './shared-link.repository'; export * from './smart-info.repository'; export * from './system-config.repository'; +export * from './system-metadata.repository'; export * from './tag.repository'; export * from './typesense.repository'; export * from './user-token.repository'; diff --git a/server/src/infra/repositories/library.repository.ts b/server/src/infra/repositories/library.repository.ts index 2b42b3cb5..155c3d9be 100644 --- a/server/src/infra/repositories/library.repository.ts +++ b/server/src/infra/repositories/library.repository.ts @@ -4,11 +4,13 @@ import { InjectRepository } from '@nestjs/typeorm'; import { IsNull, Not } from 'typeorm'; import { Repository } from 'typeorm/repository/Repository'; import { LibraryEntity, LibraryType } from '../entities'; +import { DummyValue, GenerateSql } from '../infra.util'; @Injectable() export class LibraryRepository implements ILibraryRepository { constructor(@InjectRepository(LibraryEntity) private repository: Repository) {} + @GenerateSql({ params: [DummyValue.UUID] }) get(id: string, withDeleted = false): Promise { return this.repository.findOneOrFail({ where: { @@ -19,6 +21,7 @@ export class LibraryRepository implements ILibraryRepository { }); } + @GenerateSql({ params: [DummyValue.STRING] }) existsByName(name: string, withDeleted = false): Promise { return this.repository.exist({ where: { @@ -28,10 +31,12 @@ export class LibraryRepository implements ILibraryRepository { }); } + @GenerateSql({ params: [DummyValue.UUID] }) getCountForUser(ownerId: string): Promise { return this.repository.countBy({ ownerId }); } + @GenerateSql({ params: [DummyValue.UUID] }) getDefaultUploadLibrary(ownerId: string): Promise { return this.repository.findOne({ where: { @@ -44,6 +49,7 @@ export class LibraryRepository implements ILibraryRepository { }); } + @GenerateSql({ params: [DummyValue.UUID] }) getUploadLibraryCount(ownerId: string): Promise { return this.repository.count({ where: { @@ -53,6 +59,7 @@ export class LibraryRepository implements ILibraryRepository { }); } + @GenerateSql({ params: [DummyValue.UUID] }) getAllByUserId(ownerId: string, type?: LibraryType): Promise { return this.repository.find({ where: { @@ -69,6 +76,7 @@ export class LibraryRepository implements ILibraryRepository { }); } + @GenerateSql({ params: [] }) getAll(withDeleted = false, type?: LibraryType): Promise { return this.repository.find({ where: { type }, @@ -82,6 +90,7 @@ export class LibraryRepository implements ILibraryRepository { }); } + @GenerateSql() getAllDeleted(): Promise { return this.repository.find({ where: { @@ -114,6 +123,7 @@ export class LibraryRepository implements ILibraryRepository { return this.save(library); } + @GenerateSql({ params: [DummyValue.UUID] }) async getStatistics(id: string): Promise { const stats = await this.repository .createQueryBuilder('libraries') @@ -134,6 +144,7 @@ export class LibraryRepository implements ILibraryRepository { }; } + @GenerateSql({ params: [DummyValue.UUID] }) async getOnlineAssetPaths(libraryId: string): Promise { // Return all non-offline asset paths for a given library const rawResults = await this.repository @@ -153,6 +164,7 @@ export class LibraryRepository implements ILibraryRepository { return results; } + @GenerateSql({ params: [DummyValue.UUID] }) async getAssetIds(libraryId: string, withDeleted = false): Promise { let query = await this.repository .createQueryBuilder('library') diff --git a/server/src/infra/repositories/metadata.repository.ts b/server/src/infra/repositories/metadata.repository.ts index 63bc29dcb..d8f91dd1a 100644 --- a/server/src/infra/repositories/metadata.repository.ts +++ b/server/src/infra/repositories/metadata.repository.ts @@ -1,82 +1,187 @@ -import { GeoPoint, IMetadataRepository, ImmichTags, ReverseGeocodeResult } from '@app/domain'; -import { REVERSE_GEOCODING_DUMP_DIRECTORY } from '@app/infra'; -import { Injectable, Logger } from '@nestjs/common'; -import { DefaultReadTaskOptions, exiftool } from 'exiftool-vendored'; -import { readdir, rm } from 'fs/promises'; +import { + GeoPoint, + IMetadataRepository, + ImmichTags, + ISystemMetadataRepository, + ReverseGeocodeResult, +} from '@app/domain'; +import { GeodataAdmin1Entity, GeodataAdmin2Entity, GeodataPlacesEntity, SystemMetadataKey } from '@app/infra/entities'; +import { DatabaseLock } from '@app/infra/utils/database-locks'; +import { Inject, Logger } from '@nestjs/common'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { DefaultReadTaskOptions, exiftool, Tags } from 'exiftool-vendored'; +import { createReadStream, existsSync } from 'fs'; +import { readFile } from 'fs/promises'; import * as geotz from 'geo-tz'; import { getName } from 'i18n-iso-countries'; -import geocoder, { AddressObject, InitOptions } from 'local-reverse-geocoder'; -import path from 'path'; -import { promisify } from 'util'; +import * as readLine from 'readline'; +import { DataSource, DeepPartial, QueryRunner, Repository } from 'typeorm'; -export interface AdminCode { - name: string; - asciiName: string; - geoNameId: string; -} +type GeoEntity = GeodataPlacesEntity | GeodataAdmin1Entity | GeodataAdmin2Entity; +type GeoEntityClass = typeof GeodataPlacesEntity | typeof GeodataAdmin1Entity | typeof GeodataAdmin2Entity; -export type GeoData = AddressObject & { - admin1Code?: AdminCode | string; - admin2Code?: AdminCode | string; -}; +const CITIES_FILE = 'cities500.txt'; -const lookup = promisify(geocoder.lookUp).bind(geocoder); - -@Injectable() export class MetadataRepository implements IMetadataRepository { + constructor( + @InjectRepository(GeodataPlacesEntity) private readonly geodataPlacesRepository: Repository, + @InjectRepository(GeodataAdmin1Entity) private readonly geodataAdmin1Repository: Repository, + @InjectRepository(GeodataAdmin2Entity) private readonly geodataAdmin2Repository: Repository, + @Inject(ISystemMetadataRepository) private readonly systemMetadataRepository: ISystemMetadataRepository, + @InjectDataSource() private dataSource: DataSource, + ) {} + private logger = new Logger(MetadataRepository.name); - async init(options: Partial): Promise { - return new Promise((resolve) => { - geocoder.init( - { - load: { - admin1: true, - admin2: true, - admin3And4: false, - alternateNames: false, - }, - countries: [], - dumpDirectory: REVERSE_GEOCODING_DUMP_DIRECTORY, - ...options, - }, - resolve, - ); + async init(): Promise { + this.logger.log('Initializing metadata repository'); + const geodataDate = await readFile('/usr/src/resources/geodata-date.txt', 'utf8'); + + await this.geodataPlacesRepository.query('SELECT pg_advisory_lock($1)', [DatabaseLock.GeodataImport]); + + const geocodingMetadata = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); + + if (geocodingMetadata?.lastUpdate === geodataDate) { + await this.dataSource.query('SELECT pg_advisory_unlock($1)', [DatabaseLock.GeodataImport]); + return; + } + + this.logger.log('Importing geodata to database from file'); + + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + + try { + await queryRunner.startTransaction(); + + await this.loadCities500(queryRunner); + await this.loadAdmin1(queryRunner); + await this.loadAdmin2(queryRunner); + + await queryRunner.commitTransaction(); + } catch (e) { + this.logger.fatal('Error importing geodata', e); + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); + } + + await this.systemMetadataRepository.set(SystemMetadataKey.REVERSE_GEOCODING_STATE, { + lastUpdate: geodataDate, + lastImportFileName: CITIES_FILE, }); + + await this.dataSource.query('SELECT pg_advisory_unlock($1)', [DatabaseLock.GeodataImport]); + this.logger.log('Geodata import completed'); + } + + private async loadGeodataToTableFromFile( + queryRunner: QueryRunner, + lineToEntityMapper: (lineSplit: string[]) => T, + filePath: string, + entity: GeoEntityClass, + ) { + if (!existsSync(filePath)) { + this.logger.error(`Geodata file ${filePath} not found`); + throw new Error(`Geodata file ${filePath} not found`); + } + await queryRunner.manager.clear(entity); + + const input = createReadStream(filePath); + let buffer: DeepPartial[] = []; + const lineReader = readLine.createInterface({ input: input }); + + for await (const line of lineReader) { + const lineSplit = line.split('\t'); + buffer.push(lineToEntityMapper(lineSplit)); + if (buffer.length > 1000) { + await queryRunner.manager.save(buffer); + buffer = []; + } + } + await queryRunner.manager.save(buffer); + } + + private async loadCities500(queryRunner: QueryRunner) { + await this.loadGeodataToTableFromFile( + queryRunner, + (lineSplit: string[]) => + this.geodataPlacesRepository.create({ + id: parseInt(lineSplit[0]), + name: lineSplit[1], + latitude: parseFloat(lineSplit[4]), + longitude: parseFloat(lineSplit[5]), + countryCode: lineSplit[8], + admin1Code: lineSplit[10], + admin2Code: lineSplit[11], + modificationDate: lineSplit[18], + }), + `/usr/src/resources/${CITIES_FILE}`, + GeodataPlacesEntity, + ); + } + + private async loadAdmin1(queryRunner: QueryRunner) { + await this.loadGeodataToTableFromFile( + queryRunner, + (lineSplit: string[]) => + this.geodataAdmin1Repository.create({ + key: lineSplit[0], + name: lineSplit[1], + }), + '/usr/src/resources/admin1CodesASCII.txt', + GeodataAdmin1Entity, + ); + } + + private async loadAdmin2(queryRunner: QueryRunner) { + await this.loadGeodataToTableFromFile( + queryRunner, + (lineSplit: string[]) => + this.geodataAdmin2Repository.create({ + key: lineSplit[0], + name: lineSplit[1], + }), + '/usr/src/resources/admin2Codes.txt', + GeodataAdmin2Entity, + ); } async teardown() { await exiftool.end(); } - async deleteCache() { - const dumpDirectory = REVERSE_GEOCODING_DUMP_DIRECTORY; - if (dumpDirectory) { - // delete contents - const items = await readdir(dumpDirectory, { withFileTypes: true }); - const folders = items.filter((item) => item.isDirectory()); - for (const { name } of folders) { - await rm(path.join(dumpDirectory, name), { recursive: true, force: true }); - } - } - } - - async reverseGeocode(point: GeoPoint): Promise { + async reverseGeocode(point: GeoPoint): Promise { this.logger.debug(`Request: ${point.latitude},${point.longitude}`); - const [address] = await lookup([point], 1); - this.logger.verbose(`Raw: ${JSON.stringify(address, null, 2)}`); + const response = await this.geodataPlacesRepository + .createQueryBuilder('geoplaces') + .leftJoinAndSelect('geoplaces.admin1', 'admin1') + .leftJoinAndSelect('geoplaces.admin2', 'admin2') + .where('earth_box(ll_to_earth(:latitude, :longitude), 25000) @> "earthCoord"', point) + .orderBy('earth_distance(ll_to_earth(:latitude, :longitude), "earthCoord")') + .limit(1) + .getOne(); - const { countryCode, name: city, admin1Code, admin2Code } = address[0] as GeoData; + if (!response) { + this.logger.warn( + `Response from database for reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`, + ); + return null; + } + + this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`); + + const { countryCode, name: city, admin1, admin2 } = response; const country = getName(countryCode, 'en') ?? null; - const stateParts = [(admin2Code as AdminCode)?.name, (admin1Code as AdminCode)?.name].filter((name) => !!name); + const stateParts = [admin2?.name, admin1?.name].filter((name) => !!name); const state = stateParts.length > 0 ? stateParts.join(', ') : null; - this.logger.debug(`Normalized: ${JSON.stringify({ country, state, city })}`); return { country, state, city }; } - getExifTags(path: string): Promise { + readTags(path: string): Promise { return exiftool .read(path, undefined, { ...DefaultReadTaskOptions, @@ -93,4 +198,12 @@ export class MetadataRepository implements IMetadataRepository { return null; }) as Promise; } + + async writeTags(path: string, tags: Partial): Promise { + try { + await exiftool.write(path, tags, ['-overwrite_original']); + } catch (error) { + this.logger.warn(`Error writing exif data (${path}): ${error}`); + } + } } diff --git a/server/src/infra/repositories/move.repository.ts b/server/src/infra/repositories/move.repository.ts index f909b0f20..f7995b54e 100644 --- a/server/src/infra/repositories/move.repository.ts +++ b/server/src/infra/repositories/move.repository.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { MoveEntity, PathType } from '../entities'; +import { DummyValue, GenerateSql } from '../infra.util'; @Injectable() export class MoveRepository implements IMoveRepository { @@ -12,6 +13,7 @@ export class MoveRepository implements IMoveRepository { return this.repository.save(entity); } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) getByEntity(entityId: string, pathType: PathType): Promise { return this.repository.findOne({ where: { entityId, pathType } }); } diff --git a/server/src/infra/repositories/partner.repository.ts b/server/src/infra/repositories/partner.repository.ts index c56d8b075..b5b876558 100644 --- a/server/src/infra/repositories/partner.repository.ts +++ b/server/src/infra/repositories/partner.repository.ts @@ -1,7 +1,7 @@ import { IPartnerRepository, PartnerIds } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { DeepPartial, Repository } from 'typeorm'; import { PartnerEntity } from '../entities'; @Injectable() @@ -16,12 +16,22 @@ export class PartnerRepository implements IPartnerRepository { return this.repository.findOne({ where: { sharedById, sharedWithId } }); } - async create({ sharedById, sharedWithId }: PartnerIds): Promise { - await this.repository.save({ sharedBy: { id: sharedById }, sharedWith: { id: sharedWithId } }); - return this.repository.findOneOrFail({ where: { sharedById, sharedWithId } }); + create({ sharedById, sharedWithId }: PartnerIds): Promise { + return this.save({ sharedBy: { id: sharedById }, sharedWith: { id: sharedWithId } }); } async remove(entity: PartnerEntity): Promise { await this.repository.remove(entity); } + + update(entity: Partial): Promise { + return this.save(entity); + } + + private async save(entity: DeepPartial): Promise { + await this.repository.save(entity); + return this.repository.findOneOrFail({ + where: { sharedById: entity.sharedById, sharedWithId: entity.sharedWithId }, + }); + } } diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/infra/repositories/person.repository.ts index a986a0e0c..e3c0ac26a 100644 --- a/server/src/infra/repositories/person.repository.ts +++ b/server/src/infra/repositories/person.repository.ts @@ -9,6 +9,7 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { AssetEntity, AssetFaceEntity, PersonEntity } from '../entities'; +import { DummyValue, GenerateSql } from '../infra.util'; export class PersonRepository implements IPersonRepository { constructor( @@ -36,6 +37,7 @@ export class PersonRepository implements IPersonRepository { return assetIds; } + @GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] }) async reassignFaces({ oldPersonId, newPersonId }: UpdateFacesData): Promise { const result = await this.assetFaceRepository .createQueryBuilder() @@ -57,18 +59,22 @@ export class PersonRepository implements IPersonRepository { return people.length; } + @GenerateSql() getAllFaces(): Promise { return this.assetFaceRepository.find({ relations: { asset: true }, withDeleted: true }); } + @GenerateSql() getAll(): Promise { return this.personRepository.find(); } + @GenerateSql() getAllWithoutThumbnail(): Promise { return this.personRepository.findBy({ thumbnailPath: '' }); } + @GenerateSql({ params: [DummyValue.UUID] }) getAllForUser(userId: string, options?: PersonSearchOptions): Promise { const queryBuilder = this.personRepository .createQueryBuilder('person') @@ -89,6 +95,7 @@ export class PersonRepository implements IPersonRepository { return queryBuilder.getMany(); } + @GenerateSql() getAllWithoutFaces(): Promise { return this.personRepository .createQueryBuilder('person') @@ -99,10 +106,12 @@ export class PersonRepository implements IPersonRepository { .getMany(); } + @GenerateSql({ params: [DummyValue.UUID] }) getById(personId: string): Promise { return this.personRepository.findOne({ where: { id: personId } }); } + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, { withHidden: true }] }) getByName(userId: string, personName: string, { withHidden }: PersonNameSearchOptions): Promise { const queryBuilder = this.personRepository .createQueryBuilder('person') @@ -121,6 +130,7 @@ export class PersonRepository implements IPersonRepository { return queryBuilder.getMany(); } + @GenerateSql({ params: [DummyValue.UUID] }) async getStatistics(personId: string): Promise { return { assets: await this.assetFaceRepository @@ -135,6 +145,7 @@ export class PersonRepository implements IPersonRepository { }; } + @GenerateSql({ params: [DummyValue.UUID] }) getAssets(personId: string): Promise { return this.assetRepository.find({ where: { @@ -171,10 +182,12 @@ export class PersonRepository implements IPersonRepository { return this.personRepository.findOneByOrFail({ id }); } + @GenerateSql({ params: [[{ assetId: DummyValue.UUID, personId: DummyValue.UUID }]] }) async getFacesByIds(ids: AssetFaceId[]): Promise { return this.assetFaceRepository.find({ where: ids, relations: { asset: true }, withDeleted: true }); } + @GenerateSql({ params: [DummyValue.UUID] }) async getRandomFace(personId: string): Promise { return this.assetFaceRepository.findOneBy({ personId }); } diff --git a/server/src/infra/repositories/shared-link.repository.ts b/server/src/infra/repositories/shared-link.repository.ts index 127efee43..ae8826f79 100644 --- a/server/src/infra/repositories/shared-link.repository.ts +++ b/server/src/infra/repositories/shared-link.repository.ts @@ -3,11 +3,13 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { SharedLinkEntity } from '../entities'; +import { DummyValue, GenerateSql } from '../infra.util'; @Injectable() export class SharedLinkRepository implements ISharedLinkRepository { constructor(@InjectRepository(SharedLinkEntity) private repository: Repository) {} + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) get(userId: string, id: string): Promise { return this.repository.findOne({ where: { @@ -39,6 +41,7 @@ export class SharedLinkRepository implements ISharedLinkRepository { }); } + @GenerateSql({ params: [DummyValue.UUID] }) getAll(userId: string): Promise { return this.repository.find({ where: { @@ -56,6 +59,7 @@ export class SharedLinkRepository implements ISharedLinkRepository { }); } + @GenerateSql({ params: [DummyValue.BUFFER] }) async getByKey(key: Buffer): Promise { return await this.repository.findOne({ where: { diff --git a/server/src/infra/repositories/system-config.repository.ts b/server/src/infra/repositories/system-config.repository.ts index cfe0eab3d..57eb6c1fd 100644 --- a/server/src/infra/repositories/system-config.repository.ts +++ b/server/src/infra/repositories/system-config.repository.ts @@ -1,25 +1,34 @@ import { ISystemConfigRepository } from '@app/domain'; import { InjectRepository } from '@nestjs/typeorm'; +import axios from 'axios'; import { readFile } from 'fs/promises'; import { In, Repository } from 'typeorm'; import { SystemConfigEntity } from '../entities'; +import { DummyValue, GenerateSql } from '../infra.util'; export class SystemConfigRepository implements ISystemConfigRepository { constructor( @InjectRepository(SystemConfigEntity) private repository: Repository, ) {} + async fetchStyle(url: string) { + return axios.get(url).then((response) => response.data); + } + @GenerateSql() load(): Promise { return this.repository.find(); } - readFile = readFile; + readFile(filename: string): Promise { + return readFile(filename, { encoding: 'utf-8' }); + } saveAll(items: SystemConfigEntity[]): Promise { return this.repository.save(items); } + @GenerateSql({ params: [DummyValue.STRING] }) async deleteKeys(keys: string[]): Promise { await this.repository.delete({ key: In(keys) }); } diff --git a/server/src/infra/repositories/system-metadata.repository.ts b/server/src/infra/repositories/system-metadata.repository.ts new file mode 100644 index 000000000..d43002b0b --- /dev/null +++ b/server/src/infra/repositories/system-metadata.repository.ts @@ -0,0 +1,22 @@ +import { ISystemMetadataRepository } from '@app/domain/repositories/system-metadata.repository'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { SystemMetadata, SystemMetadataEntity } from '../entities'; + +export class SystemMetadataRepository implements ISystemMetadataRepository { + constructor( + @InjectRepository(SystemMetadataEntity) + private repository: Repository, + ) {} + async get(key: T): Promise { + const metadata = await this.repository.findOne({ where: { key } }); + if (!metadata) { + return null; + } + return metadata.value as SystemMetadata[T]; + } + + async set(key: T, value: SystemMetadata[T]): Promise { + await this.repository.upsert({ key, value }, { conflictPaths: { key: true } }); + } +} diff --git a/server/src/infra/repositories/user-token.repository.ts b/server/src/infra/repositories/user-token.repository.ts index cb83134a3..b5656d922 100644 --- a/server/src/infra/repositories/user-token.repository.ts +++ b/server/src/infra/repositories/user-token.repository.ts @@ -3,11 +3,13 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { UserTokenEntity } from '../entities'; +import { DummyValue, GenerateSql } from '../infra.util'; @Injectable() export class UserTokenRepository implements IUserTokenRepository { constructor(@InjectRepository(UserTokenEntity) private repository: Repository) {} + @GenerateSql({ params: [DummyValue.STRING] }) getByToken(token: string): Promise { return this.repository.findOne({ where: { token }, relations: { user: true } }); } @@ -35,6 +37,7 @@ export class UserTokenRepository implements IUserTokenRepository { return this.repository.save(userToken); } + @GenerateSql({ params: [DummyValue.UUID] }) async delete(id: string): Promise { await this.repository.delete({ id }); } diff --git a/server/src/infra/repositories/user.repository.ts b/server/src/infra/repositories/user.repository.ts index 9809ec75d..d1b1c0d5e 100644 --- a/server/src/infra/repositories/user.repository.ts +++ b/server/src/infra/repositories/user.repository.ts @@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { IsNull, Not, Repository } from 'typeorm'; import { UserEntity } from '../entities'; +import { DummyValue, GenerateSql } from '../infra.util'; @Injectable() export class UserRepository implements IUserRepository { @@ -16,14 +17,17 @@ export class UserRepository implements IUserRepository { }); } + @GenerateSql() async getAdmin(): Promise { return this.userRepository.findOne({ where: { isAdmin: true } }); } + @GenerateSql() async hasAdmin(): Promise { return this.userRepository.exist({ where: { isAdmin: true } }); } + @GenerateSql({ params: [DummyValue.EMAIL] }) async getByEmail(email: string, withPassword?: boolean): Promise { let builder = this.userRepository.createQueryBuilder('user').where({ email }); @@ -34,10 +38,12 @@ export class UserRepository implements IUserRepository { return builder.getOne(); } + @GenerateSql({ params: [DummyValue.STRING] }) async getByStorageLabel(storageLabel: string): Promise { return this.userRepository.findOne({ where: { storageLabel } }); } + @GenerateSql({ params: [DummyValue.STRING] }) async getByOAuthId(oauthId: string): Promise { return this.userRepository.findOne({ where: { oauthId } }); } @@ -76,12 +82,12 @@ export class UserRepository implements IUserRepository { return this.userRepository.recover(user); } + @GenerateSql() async getUserStats(): Promise { const stats = await this.userRepository .createQueryBuilder('users') .select('users.id', 'userId') - .addSelect('users.firstName', 'userFirstName') - .addSelect('users.lastName', 'userLastName') + .addSelect('users.name', 'userName') .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'IMAGE' AND assets.isVisible)`, 'photos') .addSelect(`COUNT(assets.id) FILTER (WHERE assets.type = 'VIDEO' AND assets.isVisible)`, 'videos') .addSelect('COALESCE(SUM(exif.fileSizeInByte), 0)', 'usage') diff --git a/server/src/infra/sql-generator/index.ts b/server/src/infra/sql-generator/index.ts new file mode 100644 index 000000000..78b412249 --- /dev/null +++ b/server/src/infra/sql-generator/index.ts @@ -0,0 +1,168 @@ +import { INestApplication } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Test } from '@nestjs/testing'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { mkdir, rm, writeFile } from 'fs/promises'; +import { join } from 'path'; +import { databaseConfig } from '../database.config'; +import { databaseEntities } from '../entities'; +import { GENERATE_SQL_KEY, GenerateSqlQueries } from '../infra.util'; +import { + AccessRepository, + AlbumRepository, + ApiKeyRepository, + AssetRepository, + AuditRepository, + LibraryRepository, + MoveRepository, + PartnerRepository, + PersonRepository, + SharedLinkRepository, + SystemConfigRepository, + SystemMetadataRepository, + TagRepository, + UserRepository, + UserTokenRepository, +} from '../repositories'; +import { SqlLogger } from './sql.logger'; + +const reflector = new Reflector(); +const repositories = [ + AccessRepository, + AlbumRepository, + ApiKeyRepository, + AssetRepository, + AuditRepository, + LibraryRepository, + MoveRepository, + PartnerRepository, + PersonRepository, + SharedLinkRepository, + SystemConfigRepository, + SystemMetadataRepository, + TagRepository, + UserTokenRepository, + UserRepository, +]; + +type Repository = (typeof repositories)[0]; +type SqlGeneratorOptions = { targetDir: string }; + +class SqlGenerator { + private app: INestApplication | null = null; + private sqlLogger = new SqlLogger(); + private results: Record = {}; + + constructor(private options: SqlGeneratorOptions) {} + + async run() { + try { + await this.setup(); + for (const Repository of repositories) { + await this.process(Repository); + } + await this.write(); + this.stats(); + } finally { + await this.close(); + } + } + + private async setup() { + await rm(this.options.targetDir, { force: true, recursive: true }); + await mkdir(this.options.targetDir); + + const moduleFixture = await Test.createTestingModule({ + imports: [ + TypeOrmModule.forRoot({ + ...databaseConfig, + entities: databaseEntities, + logging: ['query'], + logger: this.sqlLogger, + }), + TypeOrmModule.forFeature(databaseEntities), + ], + providers: repositories, + }).compile(); + + this.app = await moduleFixture.createNestApplication().init(); + } + + async process(Repository: Repository) { + if (!this.app) { + throw new Error('Not initialized'); + } + + const data: string[] = [`-- NOTE: This file is auto generated by ./sql-generator`]; + const instance = this.app.get(Repository); + const properties = Object.getOwnPropertyNames(Repository.prototype) as Array; + for (const key of properties) { + const target = instance[key]; + if (!(target instanceof Function)) { + continue; + } + + const queries = reflector.get(GENERATE_SQL_KEY, target); + if (!queries) { + continue; + } + + // empty decorator implies calling with no arguments + if (queries.length === 0) { + queries.push({ params: [] }); + } + + for (const { name, params } of queries) { + let queryLabel = `${Repository.name}.${key}`; + if (name) { + queryLabel += ` (${name})`; + } + + this.sqlLogger.clear(); + + // errors still generate sql, which is all we care about + await target.apply(instance, params).catch(() => null); + + if (this.sqlLogger.queries.length === 0) { + console.warn(`No queries recorded for ${queryLabel}`); + continue; + } + + data.push([`-- ${queryLabel}`, ...this.sqlLogger.queries].join('\n')); + } + } + + this.results[Repository.name] = data; + } + + private async write() { + for (const [repoName, data] of Object.entries(this.results)) { + const filename = repoName.replace(/[A-Z]/g, (letter) => `.${letter.toLowerCase()}`).replace('.', ''); + const file = join(this.options.targetDir, `${filename}.sql`); + await writeFile(file, data.join('\n\n') + '\n'); + } + } + + private stats() { + console.log(`Wrote ${Object.keys(this.results).length} files`); + console.log(`Generated ${Object.values(this.results).flat().length} queries`); + } + + private async close() { + if (this.app) { + await this.app.close(); + } + } +} + +new SqlGenerator({ targetDir: './src/infra/sql' }) + .run() + .then(() => { + console.log('Done'); + process.exit(0); + }) + .catch((error) => { + console.error(error); + console.log('Something went wrong'); + process.exit(1); + }); diff --git a/server/src/infra/sql-generator/sql.logger.ts b/server/src/infra/sql-generator/sql.logger.ts new file mode 100644 index 000000000..78c3df148 --- /dev/null +++ b/server/src/infra/sql-generator/sql.logger.ts @@ -0,0 +1,27 @@ +import { Logger } from 'typeorm'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { format } = require('sql-formatter'); + +export class SqlLogger implements Logger { + queries: string[] = []; + errors: Array<{ error: string | Error; query: string }> = []; + + clear() { + this.queries = []; + this.errors = []; + } + + logQuery(query: string) { + this.queries.push(format(query, { language: 'postgresql' })); + } + + logQueryError(error: string | Error, query: string) { + this.errors.push({ error, query }); + } + + logQuerySlow() {} + logSchemaBuild() {} + logMigration() {} + log() {} +} diff --git a/server/src/infra/sql/access.repository.sql b/server/src/infra/sql/access.repository.sql new file mode 100644 index 000000000..21f9f116b --- /dev/null +++ b/server/src/infra/sql/access.repository.sql @@ -0,0 +1 @@ +-- NOTE: This file is auto generated by ./sql-generator diff --git a/server/src/infra/sql/album.repository.sql b/server/src/infra/sql/album.repository.sql new file mode 100644 index 000000000..777edb097 --- /dev/null +++ b/server/src/infra/sql/album.repository.sql @@ -0,0 +1,613 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- AlbumRepository.getById +SELECT DISTINCT + "distinctAlias"."AlbumEntity_id" AS "ids_AlbumEntity_id" +FROM + ( + SELECT + "AlbumEntity"."id" AS "AlbumEntity_id", + "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", + "AlbumEntity"."albumName" AS "AlbumEntity_albumName", + "AlbumEntity"."description" AS "AlbumEntity_description", + "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", + "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", + "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", + "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", + "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", + "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", + "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", + "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", + "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", + "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", + "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", + "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", + "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", + "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", + "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", + "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", + "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", + "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", + "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", + "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", + "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", + "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", + "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", + "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", + "AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath", + "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", + "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", + "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", + "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", + "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", + "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", + "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", + "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", + "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", + "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", + "AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId", + "AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key", + "AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type", + "AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt", + "AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt", + "AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload", + "AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload", + "AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif", + "AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId" + FROM + "albums" "AlbumEntity" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" + AND ( + "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL + ) + LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" + AND ( + "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL + ) + LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id" + WHERE + (("AlbumEntity"."id" = $1)) + AND ("AlbumEntity"."deletedAt" IS NULL) + ) "distinctAlias" +ORDER BY + "AlbumEntity_id" ASC +LIMIT + 1 + +-- AlbumRepository.getByIds +SELECT + "AlbumEntity"."id" AS "AlbumEntity_id", + "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", + "AlbumEntity"."albumName" AS "AlbumEntity_albumName", + "AlbumEntity"."description" AS "AlbumEntity_description", + "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", + "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", + "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", + "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", + "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", + "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", + "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", + "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", + "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", + "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", + "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", + "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", + "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", + "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", + "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", + "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", + "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", + "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", + "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", + "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", + "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", + "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", + "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", + "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", + "AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath", + "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", + "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", + "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", + "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", + "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", + "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", + "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled" +FROM + "albums" "AlbumEntity" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" + AND ( + "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL + ) + LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" + AND ( + "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL + ) +WHERE + (("AlbumEntity"."id" IN ($1))) + AND ("AlbumEntity"."deletedAt" IS NULL) + +-- AlbumRepository.getByAssetId +SELECT + "AlbumEntity"."id" AS "AlbumEntity_id", + "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", + "AlbumEntity"."albumName" AS "AlbumEntity_albumName", + "AlbumEntity"."description" AS "AlbumEntity_description", + "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", + "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", + "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", + "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", + "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", + "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", + "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", + "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", + "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", + "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", + "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", + "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", + "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", + "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", + "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", + "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", + "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", + "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled", + "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", + "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", + "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", + "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", + "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", + "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", + "AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath", + "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", + "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", + "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", + "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", + "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", + "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", + "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled" +FROM + "albums" "AlbumEntity" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" + AND ( + "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL + ) + LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" + AND ( + "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL + ) + LEFT JOIN "albums_assets_assets" "AlbumEntity_AlbumEntity__AlbumEntity_assets" ON "AlbumEntity_AlbumEntity__AlbumEntity_assets"."albumsId" = "AlbumEntity"."id" + LEFT JOIN "assets" "AlbumEntity__AlbumEntity_assets" ON "AlbumEntity__AlbumEntity_assets"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_assets"."assetsId" + AND ( + "AlbumEntity__AlbumEntity_assets"."deletedAt" IS NULL + ) +WHERE + ( + ( + ( + "AlbumEntity"."ownerId" = $1 + AND "AlbumEntity__AlbumEntity_assets"."id" = $2 + ) + OR ( + "AlbumEntity__AlbumEntity_sharedUsers"."id" = $3 + AND "AlbumEntity__AlbumEntity_assets"."id" = $4 + ) + ) + ) + AND ("AlbumEntity"."deletedAt" IS NULL) +ORDER BY + "AlbumEntity"."createdAt" DESC + +-- AlbumRepository.getMetadataForIds +SELECT + "album"."id" AS "album_id", + MIN("assets"."fileCreatedAt") AS "start_date", + MAX("assets"."fileCreatedAt") AS "end_date", + COUNT("album_assets"."assetsId") AS "asset_count" +FROM + "albums" "album" + LEFT JOIN "albums_assets_assets" "album_assets" ON "album_assets"."albumsId" = "album"."id" + LEFT JOIN "assets" "assets" ON "assets"."id" = "album_assets"."assetsId" + AND "assets"."deletedAt" IS NULL +WHERE + ("album"."id" IN ($1)) + AND ("album"."deletedAt" IS NULL) +GROUP BY + "album"."id" + +-- AlbumRepository.getInvalidThumbnail +SELECT + "albums"."id" AS "albums_id" +FROM + "albums" "albums" +WHERE + ( + "albums"."albumThumbnailAssetId" IS NULL + AND EXISTS ( + SELECT + 1 + FROM + "albums_assets_assets" "albums_assets" + WHERE + "albums"."id" = "albums_assets"."albumsId" + ) + OR "albums"."albumThumbnailAssetId" IS NOT NULL + AND NOT EXISTS ( + SELECT + 1 + FROM + "albums_assets_assets" "albums_assets" + WHERE + "albums"."id" = "albums_assets"."albumsId" + AND "albums"."albumThumbnailAssetId" = "albums_assets"."assetsId" + ) + ) + AND ("albums"."deletedAt" IS NULL) + +-- AlbumRepository.getOwned +SELECT + "AlbumEntity"."id" AS "AlbumEntity_id", + "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", + "AlbumEntity"."albumName" AS "AlbumEntity_albumName", + "AlbumEntity"."description" AS "AlbumEntity_description", + "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", + "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", + "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", + "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", + "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", + "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", + "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", + "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", + "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", + "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", + "AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath", + "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", + "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", + "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", + "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", + "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", + "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", + "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", + "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", + "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", + "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", + "AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId", + "AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key", + "AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type", + "AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt", + "AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt", + "AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload", + "AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload", + "AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif", + "AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId", + "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", + "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", + "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", + "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", + "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", + "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", + "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", + "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", + "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", + "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", + "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", + "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", + "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", + "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled" +FROM + "albums" "AlbumEntity" + LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" + AND ( + "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL + ) + LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" + AND ( + "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL + ) +WHERE + (("AlbumEntity"."ownerId" = $1)) + AND ("AlbumEntity"."deletedAt" IS NULL) +ORDER BY + "AlbumEntity"."createdAt" DESC + +-- AlbumRepository.getShared +SELECT + "AlbumEntity"."id" AS "AlbumEntity_id", + "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", + "AlbumEntity"."albumName" AS "AlbumEntity_albumName", + "AlbumEntity"."description" AS "AlbumEntity_description", + "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", + "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", + "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", + "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", + "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", + "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", + "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", + "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", + "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", + "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", + "AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath", + "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", + "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", + "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", + "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", + "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", + "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", + "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", + "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", + "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", + "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", + "AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId", + "AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key", + "AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type", + "AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt", + "AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt", + "AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload", + "AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload", + "AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif", + "AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId", + "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", + "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", + "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", + "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", + "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", + "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", + "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", + "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", + "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", + "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", + "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", + "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", + "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", + "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled" +FROM + "albums" "AlbumEntity" + LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" + AND ( + "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL + ) + LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" + AND ( + "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL + ) +WHERE + ( + ( + ("AlbumEntity__AlbumEntity_sharedUsers"."id" = $1) + OR ( + "AlbumEntity__AlbumEntity_sharedLinks"."userId" = $2 + ) + OR ( + "AlbumEntity"."ownerId" = $3 + AND NOT ( + "AlbumEntity__AlbumEntity_sharedUsers"."id" IS NULL + ) + ) + ) + ) + AND ("AlbumEntity"."deletedAt" IS NULL) +ORDER BY + "AlbumEntity"."createdAt" DESC + +-- AlbumRepository.getNotShared +SELECT + "AlbumEntity"."id" AS "AlbumEntity_id", + "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", + "AlbumEntity"."albumName" AS "AlbumEntity_albumName", + "AlbumEntity"."description" AS "AlbumEntity_description", + "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", + "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", + "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", + "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", + "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", + "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", + "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", + "AlbumEntity__AlbumEntity_sharedUsers"."isAdmin" AS "AlbumEntity__AlbumEntity_sharedUsers_isAdmin", + "AlbumEntity__AlbumEntity_sharedUsers"."email" AS "AlbumEntity__AlbumEntity_sharedUsers_email", + "AlbumEntity__AlbumEntity_sharedUsers"."storageLabel" AS "AlbumEntity__AlbumEntity_sharedUsers_storageLabel", + "AlbumEntity__AlbumEntity_sharedUsers"."externalPath" AS "AlbumEntity__AlbumEntity_sharedUsers_externalPath", + "AlbumEntity__AlbumEntity_sharedUsers"."oauthId" AS "AlbumEntity__AlbumEntity_sharedUsers_oauthId", + "AlbumEntity__AlbumEntity_sharedUsers"."profileImagePath" AS "AlbumEntity__AlbumEntity_sharedUsers_profileImagePath", + "AlbumEntity__AlbumEntity_sharedUsers"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_sharedUsers_shouldChangePassword", + "AlbumEntity__AlbumEntity_sharedUsers"."createdAt" AS "AlbumEntity__AlbumEntity_sharedUsers_createdAt", + "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_deletedAt", + "AlbumEntity__AlbumEntity_sharedUsers"."updatedAt" AS "AlbumEntity__AlbumEntity_sharedUsers_updatedAt", + "AlbumEntity__AlbumEntity_sharedUsers"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_sharedUsers_memoriesEnabled", + "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", + "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", + "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", + "AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId", + "AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key", + "AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type", + "AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt", + "AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt", + "AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload", + "AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload", + "AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif", + "AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId", + "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", + "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", + "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", + "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", + "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", + "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", + "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", + "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", + "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", + "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", + "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", + "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", + "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", + "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled" +FROM + "albums" "AlbumEntity" + LEFT JOIN "albums_shared_users_users" "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_sharedUsers"."usersId" + AND ( + "AlbumEntity__AlbumEntity_sharedUsers"."deletedAt" IS NULL + ) + LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" + AND ( + "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL + ) +WHERE + ( + ( + "AlbumEntity"."ownerId" = $1 + AND "AlbumEntity__AlbumEntity_sharedUsers"."id" IS NULL + AND "AlbumEntity__AlbumEntity_sharedLinks"."id" IS NULL + ) + ) + AND ("AlbumEntity"."deletedAt" IS NULL) +ORDER BY + "AlbumEntity"."createdAt" DESC + +-- AlbumRepository.getAll +SELECT + "AlbumEntity"."id" AS "AlbumEntity_id", + "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", + "AlbumEntity"."albumName" AS "AlbumEntity_albumName", + "AlbumEntity"."description" AS "AlbumEntity_description", + "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", + "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", + "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", + "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", + "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", + "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", + "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", + "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", + "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", + "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", + "AlbumEntity__AlbumEntity_owner"."externalPath" AS "AlbumEntity__AlbumEntity_owner_externalPath", + "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", + "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", + "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", + "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", + "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", + "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", + "AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled" +FROM + "albums" "AlbumEntity" + LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" + AND ( + "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL + ) +WHERE + "AlbumEntity"."deletedAt" IS NULL + +-- AlbumRepository.removeAssets +DELETE FROM "albums_assets_assets" +WHERE + ( + "albumsId" = $1 + AND "assetsId" IN ($2) + ) + +-- AlbumRepository.getAssetIds +SELECT + "albums_assets"."assetsId" AS "assetId" +FROM + "albums_assets_assets" "albums_assets" +WHERE + "albums_assets"."albumsId" = $1 + AND "albums_assets"."assetsId" IN ($2) + +-- AlbumRepository.getAssetIds (no assets) +SELECT + "albums_assets"."assetsId" AS "assetId" +FROM + "albums_assets_assets" "albums_assets" +WHERE + "albums_assets"."albumsId" = $1 + +-- AlbumRepository.hasAsset +SELECT + 1 AS "row_exists" +FROM + ( + SELECT + 1 AS dummy_column + ) "dummy_table" +WHERE + EXISTS ( + SELECT + 1 + FROM + "albums" "AlbumEntity" + LEFT JOIN "albums_assets_assets" "AlbumEntity_AlbumEntity__AlbumEntity_assets" ON "AlbumEntity_AlbumEntity__AlbumEntity_assets"."albumsId" = "AlbumEntity"."id" + LEFT JOIN "assets" "AlbumEntity__AlbumEntity_assets" ON "AlbumEntity__AlbumEntity_assets"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_assets"."assetsId" + AND ( + "AlbumEntity__AlbumEntity_assets"."deletedAt" IS NULL + ) + WHERE + ( + ( + "AlbumEntity"."id" = $1 + AND "AlbumEntity__AlbumEntity_assets"."id" = $2 + ) + ) + AND ("AlbumEntity"."deletedAt" IS NULL) + ) +LIMIT + 1 + +-- AlbumRepository.addAssets +INSERT INTO + "albums_assets_assets" ("albumsId", "assetsId") +VALUES + ($1, $2) + +-- AlbumRepository.updateThumbnails +UPDATE "albums" +SET + "albumThumbnailAssetId" = ( + SELECT + "albums_assets2"."assetsId" + FROM + "assets" "assets", + "albums_assets_assets" "albums_assets2" + WHERE + ( + "albums_assets2"."assetsId" = "assets"."id" + AND "albums_assets2"."albumsId" = "albums"."id" + ) + AND ("assets"."deletedAt" IS NULL) + ORDER BY + "assets"."fileCreatedAt" DESC + LIMIT + 1 + ), + "updatedAt" = CURRENT_TIMESTAMP +WHERE + "albums"."albumThumbnailAssetId" IS NULL + AND EXISTS ( + SELECT + 1 + FROM + "albums_assets_assets" "albums_assets" + WHERE + "albums"."id" = "albums_assets"."albumsId" + ) + OR "albums"."albumThumbnailAssetId" IS NOT NULL + AND NOT EXISTS ( + SELECT + 1 + FROM + "albums_assets_assets" "albums_assets" + WHERE + "albums"."id" = "albums_assets"."albumsId" + AND "albums"."albumThumbnailAssetId" = "albums_assets"."assetsId" + ) diff --git a/server/src/infra/sql/api.key.repository.sql b/server/src/infra/sql/api.key.repository.sql new file mode 100644 index 000000000..7f26d8575 --- /dev/null +++ b/server/src/infra/sql/api.key.repository.sql @@ -0,0 +1,69 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- ApiKeyRepository.getKey +SELECT DISTINCT + "distinctAlias"."APIKeyEntity_id" AS "ids_APIKeyEntity_id" +FROM + ( + SELECT + "APIKeyEntity"."id" AS "APIKeyEntity_id", + "APIKeyEntity"."key" AS "APIKeyEntity_key", + "APIKeyEntity"."userId" AS "APIKeyEntity_userId", + "APIKeyEntity__APIKeyEntity_user"."id" AS "APIKeyEntity__APIKeyEntity_user_id", + "APIKeyEntity__APIKeyEntity_user"."name" AS "APIKeyEntity__APIKeyEntity_user_name", + "APIKeyEntity__APIKeyEntity_user"."avatarColor" AS "APIKeyEntity__APIKeyEntity_user_avatarColor", + "APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin", + "APIKeyEntity__APIKeyEntity_user"."email" AS "APIKeyEntity__APIKeyEntity_user_email", + "APIKeyEntity__APIKeyEntity_user"."storageLabel" AS "APIKeyEntity__APIKeyEntity_user_storageLabel", + "APIKeyEntity__APIKeyEntity_user"."externalPath" AS "APIKeyEntity__APIKeyEntity_user_externalPath", + "APIKeyEntity__APIKeyEntity_user"."oauthId" AS "APIKeyEntity__APIKeyEntity_user_oauthId", + "APIKeyEntity__APIKeyEntity_user"."profileImagePath" AS "APIKeyEntity__APIKeyEntity_user_profileImagePath", + "APIKeyEntity__APIKeyEntity_user"."shouldChangePassword" AS "APIKeyEntity__APIKeyEntity_user_shouldChangePassword", + "APIKeyEntity__APIKeyEntity_user"."createdAt" AS "APIKeyEntity__APIKeyEntity_user_createdAt", + "APIKeyEntity__APIKeyEntity_user"."deletedAt" AS "APIKeyEntity__APIKeyEntity_user_deletedAt", + "APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt", + "APIKeyEntity__APIKeyEntity_user"."memoriesEnabled" AS "APIKeyEntity__APIKeyEntity_user_memoriesEnabled" + FROM + "api_keys" "APIKeyEntity" + LEFT JOIN "users" "APIKeyEntity__APIKeyEntity_user" ON "APIKeyEntity__APIKeyEntity_user"."id" = "APIKeyEntity"."userId" + AND ( + "APIKeyEntity__APIKeyEntity_user"."deletedAt" IS NULL + ) + WHERE + ("APIKeyEntity"."key" = $1) + ) "distinctAlias" +ORDER BY + "APIKeyEntity_id" ASC +LIMIT + 1 + +-- ApiKeyRepository.getById +SELECT + "APIKeyEntity"."id" AS "APIKeyEntity_id", + "APIKeyEntity"."name" AS "APIKeyEntity_name", + "APIKeyEntity"."userId" AS "APIKeyEntity_userId", + "APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt", + "APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt" +FROM + "api_keys" "APIKeyEntity" +WHERE + ( + "APIKeyEntity"."userId" = $1 + AND "APIKeyEntity"."id" = $2 + ) +LIMIT + 1 + +-- ApiKeyRepository.getByUserId +SELECT + "APIKeyEntity"."id" AS "APIKeyEntity_id", + "APIKeyEntity"."name" AS "APIKeyEntity_name", + "APIKeyEntity"."userId" AS "APIKeyEntity_userId", + "APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt", + "APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt" +FROM + "api_keys" "APIKeyEntity" +WHERE + ("APIKeyEntity"."userId" = $1) +ORDER BY + "APIKeyEntity"."createdAt" DESC diff --git a/server/src/infra/sql/asset.repository.sql b/server/src/infra/sql/asset.repository.sql new file mode 100644 index 000000000..c970c997b --- /dev/null +++ b/server/src/infra/sql/asset.repository.sql @@ -0,0 +1,614 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- AssetRepository.getByDate +SELECT + "AssetEntity"."id" AS "AssetEntity_id", + "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", + "AssetEntity"."ownerId" AS "AssetEntity_ownerId", + "AssetEntity"."libraryId" AS "AssetEntity_libraryId", + "AssetEntity"."deviceId" AS "AssetEntity_deviceId", + "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."originalPath" AS "AssetEntity_originalPath", + "AssetEntity"."resizePath" AS "AssetEntity_resizePath", + "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", + "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", + "AssetEntity"."createdAt" AS "AssetEntity_createdAt", + "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", + "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", + "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", + "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", + "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", + "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", + "AssetEntity"."isArchived" AS "AssetEntity_isArchived", + "AssetEntity"."isExternal" AS "AssetEntity_isExternal", + "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", + "AssetEntity"."isOffline" AS "AssetEntity_isOffline", + "AssetEntity"."checksum" AS "AssetEntity_checksum", + "AssetEntity"."duration" AS "AssetEntity_duration", + "AssetEntity"."isVisible" AS "AssetEntity_isVisible", + "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", + "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", + "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", + "AssetEntity"."stackParentId" AS "AssetEntity_stackParentId", + "AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId", + "AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description", + "AssetEntity__AssetEntity_exifInfo"."exifImageWidth" AS "AssetEntity__AssetEntity_exifInfo_exifImageWidth", + "AssetEntity__AssetEntity_exifInfo"."exifImageHeight" AS "AssetEntity__AssetEntity_exifInfo_exifImageHeight", + "AssetEntity__AssetEntity_exifInfo"."fileSizeInByte" AS "AssetEntity__AssetEntity_exifInfo_fileSizeInByte", + "AssetEntity__AssetEntity_exifInfo"."orientation" AS "AssetEntity__AssetEntity_exifInfo_orientation", + "AssetEntity__AssetEntity_exifInfo"."dateTimeOriginal" AS "AssetEntity__AssetEntity_exifInfo_dateTimeOriginal", + "AssetEntity__AssetEntity_exifInfo"."modifyDate" AS "AssetEntity__AssetEntity_exifInfo_modifyDate", + "AssetEntity__AssetEntity_exifInfo"."timeZone" AS "AssetEntity__AssetEntity_exifInfo_timeZone", + "AssetEntity__AssetEntity_exifInfo"."latitude" AS "AssetEntity__AssetEntity_exifInfo_latitude", + "AssetEntity__AssetEntity_exifInfo"."longitude" AS "AssetEntity__AssetEntity_exifInfo_longitude", + "AssetEntity__AssetEntity_exifInfo"."projectionType" AS "AssetEntity__AssetEntity_exifInfo_projectionType", + "AssetEntity__AssetEntity_exifInfo"."city" AS "AssetEntity__AssetEntity_exifInfo_city", + "AssetEntity__AssetEntity_exifInfo"."livePhotoCID" AS "AssetEntity__AssetEntity_exifInfo_livePhotoCID", + "AssetEntity__AssetEntity_exifInfo"."state" AS "AssetEntity__AssetEntity_exifInfo_state", + "AssetEntity__AssetEntity_exifInfo"."country" AS "AssetEntity__AssetEntity_exifInfo_country", + "AssetEntity__AssetEntity_exifInfo"."make" AS "AssetEntity__AssetEntity_exifInfo_make", + "AssetEntity__AssetEntity_exifInfo"."model" AS "AssetEntity__AssetEntity_exifInfo_model", + "AssetEntity__AssetEntity_exifInfo"."lensModel" AS "AssetEntity__AssetEntity_exifInfo_lensModel", + "AssetEntity__AssetEntity_exifInfo"."fNumber" AS "AssetEntity__AssetEntity_exifInfo_fNumber", + "AssetEntity__AssetEntity_exifInfo"."focalLength" AS "AssetEntity__AssetEntity_exifInfo_focalLength", + "AssetEntity__AssetEntity_exifInfo"."iso" AS "AssetEntity__AssetEntity_exifInfo_iso", + "AssetEntity__AssetEntity_exifInfo"."exposureTime" AS "AssetEntity__AssetEntity_exifInfo_exposureTime", + "AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription", + "AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace", + "AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample", + "AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps", + "AssetEntity__AssetEntity_exifInfo"."exifTextSearchableColumn" AS "AssetEntity__AssetEntity_exifInfo_exifTextSearchableColumn" +FROM + "assets" "AssetEntity" + LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id" +WHERE + ( + ( + "AssetEntity"."ownerId" = $1 + AND "AssetEntity"."isVisible" = $2 + AND "AssetEntity"."isArchived" = $3 + AND NOT ("AssetEntity"."resizePath" IS NULL) + AND "AssetEntity"."fileCreatedAt" BETWEEN $4 AND $5 + ) + ) + AND ("AssetEntity"."deletedAt" IS NULL) +ORDER BY + "AssetEntity"."fileCreatedAt" DESC + +-- AssetRepository.getByDayOfYear +SELECT + "entity"."id" AS "entity_id", + "entity"."deviceAssetId" AS "entity_deviceAssetId", + "entity"."ownerId" AS "entity_ownerId", + "entity"."libraryId" AS "entity_libraryId", + "entity"."deviceId" AS "entity_deviceId", + "entity"."type" AS "entity_type", + "entity"."originalPath" AS "entity_originalPath", + "entity"."resizePath" AS "entity_resizePath", + "entity"."webpPath" AS "entity_webpPath", + "entity"."thumbhash" AS "entity_thumbhash", + "entity"."encodedVideoPath" AS "entity_encodedVideoPath", + "entity"."createdAt" AS "entity_createdAt", + "entity"."updatedAt" AS "entity_updatedAt", + "entity"."deletedAt" AS "entity_deletedAt", + "entity"."fileCreatedAt" AS "entity_fileCreatedAt", + "entity"."localDateTime" AS "entity_localDateTime", + "entity"."fileModifiedAt" AS "entity_fileModifiedAt", + "entity"."isFavorite" AS "entity_isFavorite", + "entity"."isArchived" AS "entity_isArchived", + "entity"."isExternal" AS "entity_isExternal", + "entity"."isReadOnly" AS "entity_isReadOnly", + "entity"."isOffline" AS "entity_isOffline", + "entity"."checksum" AS "entity_checksum", + "entity"."duration" AS "entity_duration", + "entity"."isVisible" AS "entity_isVisible", + "entity"."livePhotoVideoId" AS "entity_livePhotoVideoId", + "entity"."originalFileName" AS "entity_originalFileName", + "entity"."sidecarPath" AS "entity_sidecarPath", + "entity"."stackParentId" AS "entity_stackParentId", + "exifInfo"."assetId" AS "exifInfo_assetId", + "exifInfo"."description" AS "exifInfo_description", + "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", + "exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight", + "exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte", + "exifInfo"."orientation" AS "exifInfo_orientation", + "exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal", + "exifInfo"."modifyDate" AS "exifInfo_modifyDate", + "exifInfo"."timeZone" AS "exifInfo_timeZone", + "exifInfo"."latitude" AS "exifInfo_latitude", + "exifInfo"."longitude" AS "exifInfo_longitude", + "exifInfo"."projectionType" AS "exifInfo_projectionType", + "exifInfo"."city" AS "exifInfo_city", + "exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID", + "exifInfo"."state" AS "exifInfo_state", + "exifInfo"."country" AS "exifInfo_country", + "exifInfo"."make" AS "exifInfo_make", + "exifInfo"."model" AS "exifInfo_model", + "exifInfo"."lensModel" AS "exifInfo_lensModel", + "exifInfo"."fNumber" AS "exifInfo_fNumber", + "exifInfo"."focalLength" AS "exifInfo_focalLength", + "exifInfo"."iso" AS "exifInfo_iso", + "exifInfo"."exposureTime" AS "exifInfo_exposureTime", + "exifInfo"."profileDescription" AS "exifInfo_profileDescription", + "exifInfo"."colorspace" AS "exifInfo_colorspace", + "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."fps" AS "exifInfo_fps", + "exifInfo"."exifTextSearchableColumn" AS "exifInfo_exifTextSearchableColumn" +FROM + "assets" "entity" + LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "entity"."id" +WHERE + ( + "entity"."ownerId" = $1 + AND "entity"."isVisible" = true + AND "entity"."isArchived" = false + AND "entity"."resizePath" IS NOT NULL + AND EXTRACT( + DAY + FROM + "entity"."localDateTime" AT TIME ZONE 'UTC' + ) = $2 + AND EXTRACT( + MONTH + FROM + "entity"."localDateTime" AT TIME ZONE 'UTC' + ) = $3 + ) + AND ("entity"."deletedAt" IS NULL) +ORDER BY + "entity"."localDateTime" DESC + +-- AssetRepository.getByIds +SELECT + "AssetEntity"."id" AS "AssetEntity_id", + "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", + "AssetEntity"."ownerId" AS "AssetEntity_ownerId", + "AssetEntity"."libraryId" AS "AssetEntity_libraryId", + "AssetEntity"."deviceId" AS "AssetEntity_deviceId", + "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."originalPath" AS "AssetEntity_originalPath", + "AssetEntity"."resizePath" AS "AssetEntity_resizePath", + "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", + "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", + "AssetEntity"."createdAt" AS "AssetEntity_createdAt", + "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", + "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", + "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", + "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", + "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", + "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", + "AssetEntity"."isArchived" AS "AssetEntity_isArchived", + "AssetEntity"."isExternal" AS "AssetEntity_isExternal", + "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", + "AssetEntity"."isOffline" AS "AssetEntity_isOffline", + "AssetEntity"."checksum" AS "AssetEntity_checksum", + "AssetEntity"."duration" AS "AssetEntity_duration", + "AssetEntity"."isVisible" AS "AssetEntity_isVisible", + "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", + "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", + "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", + "AssetEntity"."stackParentId" AS "AssetEntity_stackParentId", + "AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId", + "AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description", + "AssetEntity__AssetEntity_exifInfo"."exifImageWidth" AS "AssetEntity__AssetEntity_exifInfo_exifImageWidth", + "AssetEntity__AssetEntity_exifInfo"."exifImageHeight" AS "AssetEntity__AssetEntity_exifInfo_exifImageHeight", + "AssetEntity__AssetEntity_exifInfo"."fileSizeInByte" AS "AssetEntity__AssetEntity_exifInfo_fileSizeInByte", + "AssetEntity__AssetEntity_exifInfo"."orientation" AS "AssetEntity__AssetEntity_exifInfo_orientation", + "AssetEntity__AssetEntity_exifInfo"."dateTimeOriginal" AS "AssetEntity__AssetEntity_exifInfo_dateTimeOriginal", + "AssetEntity__AssetEntity_exifInfo"."modifyDate" AS "AssetEntity__AssetEntity_exifInfo_modifyDate", + "AssetEntity__AssetEntity_exifInfo"."timeZone" AS "AssetEntity__AssetEntity_exifInfo_timeZone", + "AssetEntity__AssetEntity_exifInfo"."latitude" AS "AssetEntity__AssetEntity_exifInfo_latitude", + "AssetEntity__AssetEntity_exifInfo"."longitude" AS "AssetEntity__AssetEntity_exifInfo_longitude", + "AssetEntity__AssetEntity_exifInfo"."projectionType" AS "AssetEntity__AssetEntity_exifInfo_projectionType", + "AssetEntity__AssetEntity_exifInfo"."city" AS "AssetEntity__AssetEntity_exifInfo_city", + "AssetEntity__AssetEntity_exifInfo"."livePhotoCID" AS "AssetEntity__AssetEntity_exifInfo_livePhotoCID", + "AssetEntity__AssetEntity_exifInfo"."state" AS "AssetEntity__AssetEntity_exifInfo_state", + "AssetEntity__AssetEntity_exifInfo"."country" AS "AssetEntity__AssetEntity_exifInfo_country", + "AssetEntity__AssetEntity_exifInfo"."make" AS "AssetEntity__AssetEntity_exifInfo_make", + "AssetEntity__AssetEntity_exifInfo"."model" AS "AssetEntity__AssetEntity_exifInfo_model", + "AssetEntity__AssetEntity_exifInfo"."lensModel" AS "AssetEntity__AssetEntity_exifInfo_lensModel", + "AssetEntity__AssetEntity_exifInfo"."fNumber" AS "AssetEntity__AssetEntity_exifInfo_fNumber", + "AssetEntity__AssetEntity_exifInfo"."focalLength" AS "AssetEntity__AssetEntity_exifInfo_focalLength", + "AssetEntity__AssetEntity_exifInfo"."iso" AS "AssetEntity__AssetEntity_exifInfo_iso", + "AssetEntity__AssetEntity_exifInfo"."exposureTime" AS "AssetEntity__AssetEntity_exifInfo_exposureTime", + "AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription", + "AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace", + "AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample", + "AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps", + "AssetEntity__AssetEntity_exifInfo"."exifTextSearchableColumn" AS "AssetEntity__AssetEntity_exifInfo_exifTextSearchableColumn", + "AssetEntity__AssetEntity_smartInfo"."assetId" AS "AssetEntity__AssetEntity_smartInfo_assetId", + "AssetEntity__AssetEntity_smartInfo"."tags" AS "AssetEntity__AssetEntity_smartInfo_tags", + "AssetEntity__AssetEntity_smartInfo"."objects" AS "AssetEntity__AssetEntity_smartInfo_objects", + "AssetEntity__AssetEntity_smartInfo"."clipEmbedding" AS "AssetEntity__AssetEntity_smartInfo_clipEmbedding", + "AssetEntity__AssetEntity_tags"."id" AS "AssetEntity__AssetEntity_tags_id", + "AssetEntity__AssetEntity_tags"."type" AS "AssetEntity__AssetEntity_tags_type", + "AssetEntity__AssetEntity_tags"."name" AS "AssetEntity__AssetEntity_tags_name", + "AssetEntity__AssetEntity_tags"."userId" AS "AssetEntity__AssetEntity_tags_userId", + "AssetEntity__AssetEntity_tags"."renameTagId" AS "AssetEntity__AssetEntity_tags_renameTagId", + "AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id", + "AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId", + "AssetEntity__AssetEntity_faces"."personId" AS "AssetEntity__AssetEntity_faces_personId", + "AssetEntity__AssetEntity_faces"."embedding" AS "AssetEntity__AssetEntity_faces_embedding", + "AssetEntity__AssetEntity_faces"."imageWidth" AS "AssetEntity__AssetEntity_faces_imageWidth", + "AssetEntity__AssetEntity_faces"."imageHeight" AS "AssetEntity__AssetEntity_faces_imageHeight", + "AssetEntity__AssetEntity_faces"."boundingBoxX1" AS "AssetEntity__AssetEntity_faces_boundingBoxX1", + "AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1", + "AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2", + "AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."ownerId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_ownerId", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."name" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_name", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."birthDate" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_birthDate", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."thumbnailPath" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_thumbnailPath", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden", + "AssetEntity__AssetEntity_stack"."id" AS "AssetEntity__AssetEntity_stack_id", + "AssetEntity__AssetEntity_stack"."deviceAssetId" AS "AssetEntity__AssetEntity_stack_deviceAssetId", + "AssetEntity__AssetEntity_stack"."ownerId" AS "AssetEntity__AssetEntity_stack_ownerId", + "AssetEntity__AssetEntity_stack"."libraryId" AS "AssetEntity__AssetEntity_stack_libraryId", + "AssetEntity__AssetEntity_stack"."deviceId" AS "AssetEntity__AssetEntity_stack_deviceId", + "AssetEntity__AssetEntity_stack"."type" AS "AssetEntity__AssetEntity_stack_type", + "AssetEntity__AssetEntity_stack"."originalPath" AS "AssetEntity__AssetEntity_stack_originalPath", + "AssetEntity__AssetEntity_stack"."resizePath" AS "AssetEntity__AssetEntity_stack_resizePath", + "AssetEntity__AssetEntity_stack"."webpPath" AS "AssetEntity__AssetEntity_stack_webpPath", + "AssetEntity__AssetEntity_stack"."thumbhash" AS "AssetEntity__AssetEntity_stack_thumbhash", + "AssetEntity__AssetEntity_stack"."encodedVideoPath" AS "AssetEntity__AssetEntity_stack_encodedVideoPath", + "AssetEntity__AssetEntity_stack"."createdAt" AS "AssetEntity__AssetEntity_stack_createdAt", + "AssetEntity__AssetEntity_stack"."updatedAt" AS "AssetEntity__AssetEntity_stack_updatedAt", + "AssetEntity__AssetEntity_stack"."deletedAt" AS "AssetEntity__AssetEntity_stack_deletedAt", + "AssetEntity__AssetEntity_stack"."fileCreatedAt" AS "AssetEntity__AssetEntity_stack_fileCreatedAt", + "AssetEntity__AssetEntity_stack"."localDateTime" AS "AssetEntity__AssetEntity_stack_localDateTime", + "AssetEntity__AssetEntity_stack"."fileModifiedAt" AS "AssetEntity__AssetEntity_stack_fileModifiedAt", + "AssetEntity__AssetEntity_stack"."isFavorite" AS "AssetEntity__AssetEntity_stack_isFavorite", + "AssetEntity__AssetEntity_stack"."isArchived" AS "AssetEntity__AssetEntity_stack_isArchived", + "AssetEntity__AssetEntity_stack"."isExternal" AS "AssetEntity__AssetEntity_stack_isExternal", + "AssetEntity__AssetEntity_stack"."isReadOnly" AS "AssetEntity__AssetEntity_stack_isReadOnly", + "AssetEntity__AssetEntity_stack"."isOffline" AS "AssetEntity__AssetEntity_stack_isOffline", + "AssetEntity__AssetEntity_stack"."checksum" AS "AssetEntity__AssetEntity_stack_checksum", + "AssetEntity__AssetEntity_stack"."duration" AS "AssetEntity__AssetEntity_stack_duration", + "AssetEntity__AssetEntity_stack"."isVisible" AS "AssetEntity__AssetEntity_stack_isVisible", + "AssetEntity__AssetEntity_stack"."livePhotoVideoId" AS "AssetEntity__AssetEntity_stack_livePhotoVideoId", + "AssetEntity__AssetEntity_stack"."originalFileName" AS "AssetEntity__AssetEntity_stack_originalFileName", + "AssetEntity__AssetEntity_stack"."sidecarPath" AS "AssetEntity__AssetEntity_stack_sidecarPath", + "AssetEntity__AssetEntity_stack"."stackParentId" AS "AssetEntity__AssetEntity_stack_stackParentId" +FROM + "assets" "AssetEntity" + LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id" + LEFT JOIN "smart_info" "AssetEntity__AssetEntity_smartInfo" ON "AssetEntity__AssetEntity_smartInfo"."assetId" = "AssetEntity"."id" + LEFT JOIN "tag_asset" "AssetEntity_AssetEntity__AssetEntity_tags" ON "AssetEntity_AssetEntity__AssetEntity_tags"."assetsId" = "AssetEntity"."id" + LEFT JOIN "tags" "AssetEntity__AssetEntity_tags" ON "AssetEntity__AssetEntity_tags"."id" = "AssetEntity_AssetEntity__AssetEntity_tags"."tagsId" + LEFT JOIN "asset_faces" "AssetEntity__AssetEntity_faces" ON "AssetEntity__AssetEntity_faces"."assetId" = "AssetEntity"."id" + LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId" + LEFT JOIN "assets" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."stackParentId" = "AssetEntity"."id" +WHERE + ("AssetEntity"."id" IN ($1)) + +-- AssetRepository.deleteAll +DELETE FROM "assets" +WHERE + "ownerId" = $1 + +-- AssetRepository.getByLibraryId +SELECT + "AssetEntity"."id" AS "AssetEntity_id", + "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", + "AssetEntity"."ownerId" AS "AssetEntity_ownerId", + "AssetEntity"."libraryId" AS "AssetEntity_libraryId", + "AssetEntity"."deviceId" AS "AssetEntity_deviceId", + "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."originalPath" AS "AssetEntity_originalPath", + "AssetEntity"."resizePath" AS "AssetEntity_resizePath", + "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", + "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", + "AssetEntity"."createdAt" AS "AssetEntity_createdAt", + "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", + "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", + "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", + "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", + "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", + "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", + "AssetEntity"."isArchived" AS "AssetEntity_isArchived", + "AssetEntity"."isExternal" AS "AssetEntity_isExternal", + "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", + "AssetEntity"."isOffline" AS "AssetEntity_isOffline", + "AssetEntity"."checksum" AS "AssetEntity_checksum", + "AssetEntity"."duration" AS "AssetEntity_duration", + "AssetEntity"."isVisible" AS "AssetEntity_isVisible", + "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", + "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", + "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", + "AssetEntity"."stackParentId" AS "AssetEntity_stackParentId" +FROM + "assets" "AssetEntity" + LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId" + AND ( + "AssetEntity__AssetEntity_library"."deletedAt" IS NULL + ) +WHERE + (("AssetEntity__AssetEntity_library"."id" IN ($1))) + AND ("AssetEntity"."deletedAt" IS NULL) + +-- AssetRepository.getByLibraryIdAndOriginalPath +SELECT DISTINCT + "distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id" +FROM + ( + SELECT + "AssetEntity"."id" AS "AssetEntity_id", + "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", + "AssetEntity"."ownerId" AS "AssetEntity_ownerId", + "AssetEntity"."libraryId" AS "AssetEntity_libraryId", + "AssetEntity"."deviceId" AS "AssetEntity_deviceId", + "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."originalPath" AS "AssetEntity_originalPath", + "AssetEntity"."resizePath" AS "AssetEntity_resizePath", + "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", + "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", + "AssetEntity"."createdAt" AS "AssetEntity_createdAt", + "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", + "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", + "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", + "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", + "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", + "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", + "AssetEntity"."isArchived" AS "AssetEntity_isArchived", + "AssetEntity"."isExternal" AS "AssetEntity_isExternal", + "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", + "AssetEntity"."isOffline" AS "AssetEntity_isOffline", + "AssetEntity"."checksum" AS "AssetEntity_checksum", + "AssetEntity"."duration" AS "AssetEntity_duration", + "AssetEntity"."isVisible" AS "AssetEntity_isVisible", + "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", + "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", + "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", + "AssetEntity"."stackParentId" AS "AssetEntity_stackParentId" + FROM + "assets" "AssetEntity" + LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId" + AND ( + "AssetEntity__AssetEntity_library"."deletedAt" IS NULL + ) + WHERE + ( + ( + "AssetEntity__AssetEntity_library"."id" = $1 + AND "AssetEntity"."originalPath" = $2 + ) + ) + AND ("AssetEntity"."deletedAt" IS NULL) + ) "distinctAlias" +ORDER BY + "AssetEntity_id" ASC +LIMIT + 1 + +-- AssetRepository.getAllByDeviceId +SELECT + "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", + "AssetEntity"."id" AS "AssetEntity_id" +FROM + "assets" "AssetEntity" +WHERE + ( + "AssetEntity"."ownerId" = $1 + AND "AssetEntity"."deviceId" = $2 + AND "AssetEntity"."isVisible" = $3 + ) + +-- AssetRepository.getById +SELECT DISTINCT + "distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id" +FROM + ( + SELECT + "AssetEntity"."id" AS "AssetEntity_id", + "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", + "AssetEntity"."ownerId" AS "AssetEntity_ownerId", + "AssetEntity"."libraryId" AS "AssetEntity_libraryId", + "AssetEntity"."deviceId" AS "AssetEntity_deviceId", + "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."originalPath" AS "AssetEntity_originalPath", + "AssetEntity"."resizePath" AS "AssetEntity_resizePath", + "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", + "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", + "AssetEntity"."createdAt" AS "AssetEntity_createdAt", + "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", + "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", + "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", + "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", + "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", + "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", + "AssetEntity"."isArchived" AS "AssetEntity_isArchived", + "AssetEntity"."isExternal" AS "AssetEntity_isExternal", + "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", + "AssetEntity"."isOffline" AS "AssetEntity_isOffline", + "AssetEntity"."checksum" AS "AssetEntity_checksum", + "AssetEntity"."duration" AS "AssetEntity_duration", + "AssetEntity"."isVisible" AS "AssetEntity_isVisible", + "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", + "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", + "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", + "AssetEntity"."stackParentId" AS "AssetEntity_stackParentId", + "AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id", + "AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId", + "AssetEntity__AssetEntity_faces"."personId" AS "AssetEntity__AssetEntity_faces_personId", + "AssetEntity__AssetEntity_faces"."embedding" AS "AssetEntity__AssetEntity_faces_embedding", + "AssetEntity__AssetEntity_faces"."imageWidth" AS "AssetEntity__AssetEntity_faces_imageWidth", + "AssetEntity__AssetEntity_faces"."imageHeight" AS "AssetEntity__AssetEntity_faces_imageHeight", + "AssetEntity__AssetEntity_faces"."boundingBoxX1" AS "AssetEntity__AssetEntity_faces_boundingBoxX1", + "AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1", + "AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2", + "AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."ownerId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_ownerId", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."name" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_name", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."birthDate" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_birthDate", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."thumbnailPath" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_thumbnailPath", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden", + "AssetEntity__AssetEntity_library"."id" AS "AssetEntity__AssetEntity_library_id", + "AssetEntity__AssetEntity_library"."name" AS "AssetEntity__AssetEntity_library_name", + "AssetEntity__AssetEntity_library"."ownerId" AS "AssetEntity__AssetEntity_library_ownerId", + "AssetEntity__AssetEntity_library"."type" AS "AssetEntity__AssetEntity_library_type", + "AssetEntity__AssetEntity_library"."importPaths" AS "AssetEntity__AssetEntity_library_importPaths", + "AssetEntity__AssetEntity_library"."exclusionPatterns" AS "AssetEntity__AssetEntity_library_exclusionPatterns", + "AssetEntity__AssetEntity_library"."createdAt" AS "AssetEntity__AssetEntity_library_createdAt", + "AssetEntity__AssetEntity_library"."updatedAt" AS "AssetEntity__AssetEntity_library_updatedAt", + "AssetEntity__AssetEntity_library"."deletedAt" AS "AssetEntity__AssetEntity_library_deletedAt", + "AssetEntity__AssetEntity_library"."refreshedAt" AS "AssetEntity__AssetEntity_library_refreshedAt", + "AssetEntity__AssetEntity_library"."isVisible" AS "AssetEntity__AssetEntity_library_isVisible", + "AssetEntity__AssetEntity_stack"."id" AS "AssetEntity__AssetEntity_stack_id", + "AssetEntity__AssetEntity_stack"."deviceAssetId" AS "AssetEntity__AssetEntity_stack_deviceAssetId", + "AssetEntity__AssetEntity_stack"."ownerId" AS "AssetEntity__AssetEntity_stack_ownerId", + "AssetEntity__AssetEntity_stack"."libraryId" AS "AssetEntity__AssetEntity_stack_libraryId", + "AssetEntity__AssetEntity_stack"."deviceId" AS "AssetEntity__AssetEntity_stack_deviceId", + "AssetEntity__AssetEntity_stack"."type" AS "AssetEntity__AssetEntity_stack_type", + "AssetEntity__AssetEntity_stack"."originalPath" AS "AssetEntity__AssetEntity_stack_originalPath", + "AssetEntity__AssetEntity_stack"."resizePath" AS "AssetEntity__AssetEntity_stack_resizePath", + "AssetEntity__AssetEntity_stack"."webpPath" AS "AssetEntity__AssetEntity_stack_webpPath", + "AssetEntity__AssetEntity_stack"."thumbhash" AS "AssetEntity__AssetEntity_stack_thumbhash", + "AssetEntity__AssetEntity_stack"."encodedVideoPath" AS "AssetEntity__AssetEntity_stack_encodedVideoPath", + "AssetEntity__AssetEntity_stack"."createdAt" AS "AssetEntity__AssetEntity_stack_createdAt", + "AssetEntity__AssetEntity_stack"."updatedAt" AS "AssetEntity__AssetEntity_stack_updatedAt", + "AssetEntity__AssetEntity_stack"."deletedAt" AS "AssetEntity__AssetEntity_stack_deletedAt", + "AssetEntity__AssetEntity_stack"."fileCreatedAt" AS "AssetEntity__AssetEntity_stack_fileCreatedAt", + "AssetEntity__AssetEntity_stack"."localDateTime" AS "AssetEntity__AssetEntity_stack_localDateTime", + "AssetEntity__AssetEntity_stack"."fileModifiedAt" AS "AssetEntity__AssetEntity_stack_fileModifiedAt", + "AssetEntity__AssetEntity_stack"."isFavorite" AS "AssetEntity__AssetEntity_stack_isFavorite", + "AssetEntity__AssetEntity_stack"."isArchived" AS "AssetEntity__AssetEntity_stack_isArchived", + "AssetEntity__AssetEntity_stack"."isExternal" AS "AssetEntity__AssetEntity_stack_isExternal", + "AssetEntity__AssetEntity_stack"."isReadOnly" AS "AssetEntity__AssetEntity_stack_isReadOnly", + "AssetEntity__AssetEntity_stack"."isOffline" AS "AssetEntity__AssetEntity_stack_isOffline", + "AssetEntity__AssetEntity_stack"."checksum" AS "AssetEntity__AssetEntity_stack_checksum", + "AssetEntity__AssetEntity_stack"."duration" AS "AssetEntity__AssetEntity_stack_duration", + "AssetEntity__AssetEntity_stack"."isVisible" AS "AssetEntity__AssetEntity_stack_isVisible", + "AssetEntity__AssetEntity_stack"."livePhotoVideoId" AS "AssetEntity__AssetEntity_stack_livePhotoVideoId", + "AssetEntity__AssetEntity_stack"."originalFileName" AS "AssetEntity__AssetEntity_stack_originalFileName", + "AssetEntity__AssetEntity_stack"."sidecarPath" AS "AssetEntity__AssetEntity_stack_sidecarPath", + "AssetEntity__AssetEntity_stack"."stackParentId" AS "AssetEntity__AssetEntity_stack_stackParentId" + FROM + "assets" "AssetEntity" + LEFT JOIN "asset_faces" "AssetEntity__AssetEntity_faces" ON "AssetEntity__AssetEntity_faces"."assetId" = "AssetEntity"."id" + LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId" + LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId" + LEFT JOIN "assets" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."stackParentId" = "AssetEntity"."id" + WHERE + ("AssetEntity"."id" = $1) + ) "distinctAlias" +ORDER BY + "AssetEntity_id" ASC +LIMIT + 1 + +-- AssetRepository.updateAll +UPDATE "assets" +SET + "deviceId" = $1, + "updatedAt" = CURRENT_TIMESTAMP +WHERE + "id" IN ($2) + +-- AssetRepository.getByChecksum +SELECT + "AssetEntity"."id" AS "AssetEntity_id", + "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", + "AssetEntity"."ownerId" AS "AssetEntity_ownerId", + "AssetEntity"."libraryId" AS "AssetEntity_libraryId", + "AssetEntity"."deviceId" AS "AssetEntity_deviceId", + "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."originalPath" AS "AssetEntity_originalPath", + "AssetEntity"."resizePath" AS "AssetEntity_resizePath", + "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", + "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", + "AssetEntity"."createdAt" AS "AssetEntity_createdAt", + "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", + "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", + "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", + "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", + "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", + "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", + "AssetEntity"."isArchived" AS "AssetEntity_isArchived", + "AssetEntity"."isExternal" AS "AssetEntity_isExternal", + "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", + "AssetEntity"."isOffline" AS "AssetEntity_isOffline", + "AssetEntity"."checksum" AS "AssetEntity_checksum", + "AssetEntity"."duration" AS "AssetEntity_duration", + "AssetEntity"."isVisible" AS "AssetEntity_isVisible", + "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", + "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", + "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", + "AssetEntity"."stackParentId" AS "AssetEntity_stackParentId" +FROM + "assets" "AssetEntity" +WHERE + ( + ( + "AssetEntity"."ownerId" = $1 + AND "AssetEntity"."checksum" = $2 + ) + ) + AND ("AssetEntity"."deletedAt" IS NULL) +LIMIT + 1 + +-- AssetRepository.getWithout (sidecar) +SELECT + "AssetEntity"."id" AS "AssetEntity_id", + "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", + "AssetEntity"."ownerId" AS "AssetEntity_ownerId", + "AssetEntity"."libraryId" AS "AssetEntity_libraryId", + "AssetEntity"."deviceId" AS "AssetEntity_deviceId", + "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."originalPath" AS "AssetEntity_originalPath", + "AssetEntity"."resizePath" AS "AssetEntity_resizePath", + "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", + "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", + "AssetEntity"."createdAt" AS "AssetEntity_createdAt", + "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", + "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", + "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", + "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", + "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", + "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", + "AssetEntity"."isArchived" AS "AssetEntity_isArchived", + "AssetEntity"."isExternal" AS "AssetEntity_isExternal", + "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", + "AssetEntity"."isOffline" AS "AssetEntity_isOffline", + "AssetEntity"."checksum" AS "AssetEntity_checksum", + "AssetEntity"."duration" AS "AssetEntity_duration", + "AssetEntity"."isVisible" AS "AssetEntity_isVisible", + "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", + "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", + "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", + "AssetEntity"."stackParentId" AS "AssetEntity_stackParentId" +FROM + "assets" "AssetEntity" +WHERE + ( + ( + ( + "AssetEntity"."sidecarPath" IS NULL + AND "AssetEntity"."isVisible" = $1 + ) + OR ( + "AssetEntity"."sidecarPath" = $2 + AND "AssetEntity"."isVisible" = $3 + ) + ) + ) + AND ("AssetEntity"."deletedAt" IS NULL) +ORDER BY + "AssetEntity"."createdAt" ASC +LIMIT + 11 diff --git a/server/src/infra/sql/audit.repository.sql b/server/src/infra/sql/audit.repository.sql new file mode 100644 index 000000000..21f9f116b --- /dev/null +++ b/server/src/infra/sql/audit.repository.sql @@ -0,0 +1 @@ +-- NOTE: This file is auto generated by ./sql-generator diff --git a/server/src/infra/sql/library.repository.sql b/server/src/infra/sql/library.repository.sql new file mode 100644 index 000000000..44e6ddcc9 --- /dev/null +++ b/server/src/infra/sql/library.repository.sql @@ -0,0 +1,299 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- LibraryRepository.get +SELECT DISTINCT + "distinctAlias"."LibraryEntity_id" AS "ids_LibraryEntity_id" +FROM + ( + SELECT + "LibraryEntity"."id" AS "LibraryEntity_id", + "LibraryEntity"."name" AS "LibraryEntity_name", + "LibraryEntity"."ownerId" AS "LibraryEntity_ownerId", + "LibraryEntity"."type" AS "LibraryEntity_type", + "LibraryEntity"."importPaths" AS "LibraryEntity_importPaths", + "LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns", + "LibraryEntity"."createdAt" AS "LibraryEntity_createdAt", + "LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt", + "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", + "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", + "LibraryEntity"."isVisible" AS "LibraryEntity_isVisible", + "LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id", + "LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name", + "LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor", + "LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin", + "LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email", + "LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel", + "LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath", + "LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId", + "LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath", + "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", + "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", + "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", + "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", + "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled" + FROM + "libraries" "LibraryEntity" + LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" + AND ( + "LibraryEntity__LibraryEntity_owner"."deletedAt" IS NULL + ) + WHERE + (("LibraryEntity"."id" = $1)) + AND ("LibraryEntity"."deletedAt" IS NULL) + ) "distinctAlias" +ORDER BY + "LibraryEntity_id" ASC +LIMIT + 1 + +-- LibraryRepository.existsByName +SELECT + 1 AS "row_exists" +FROM + ( + SELECT + 1 AS dummy_column + ) "dummy_table" +WHERE + EXISTS ( + SELECT + 1 + FROM + "libraries" "LibraryEntity" + WHERE + (("LibraryEntity"."name" = $1)) + AND ("LibraryEntity"."deletedAt" IS NULL) + ) +LIMIT + 1 + +-- LibraryRepository.getCountForUser +SELECT + COUNT(1) AS "cnt" +FROM + "libraries" "LibraryEntity" +WHERE + (("LibraryEntity"."ownerId" = $1)) + AND ("LibraryEntity"."deletedAt" IS NULL) + +-- LibraryRepository.getDefaultUploadLibrary +SELECT + "LibraryEntity"."id" AS "LibraryEntity_id", + "LibraryEntity"."name" AS "LibraryEntity_name", + "LibraryEntity"."ownerId" AS "LibraryEntity_ownerId", + "LibraryEntity"."type" AS "LibraryEntity_type", + "LibraryEntity"."importPaths" AS "LibraryEntity_importPaths", + "LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns", + "LibraryEntity"."createdAt" AS "LibraryEntity_createdAt", + "LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt", + "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", + "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", + "LibraryEntity"."isVisible" AS "LibraryEntity_isVisible" +FROM + "libraries" "LibraryEntity" +WHERE + ( + ( + "LibraryEntity"."ownerId" = $1 + AND "LibraryEntity"."type" = $2 + ) + ) + AND ("LibraryEntity"."deletedAt" IS NULL) +ORDER BY + "LibraryEntity"."createdAt" ASC +LIMIT + 1 + +-- LibraryRepository.getUploadLibraryCount +SELECT + COUNT(1) AS "cnt" +FROM + "libraries" "LibraryEntity" +WHERE + ( + ( + "LibraryEntity"."ownerId" = $1 + AND "LibraryEntity"."type" = $2 + ) + ) + AND ("LibraryEntity"."deletedAt" IS NULL) + +-- LibraryRepository.getAllByUserId +SELECT + "LibraryEntity"."id" AS "LibraryEntity_id", + "LibraryEntity"."name" AS "LibraryEntity_name", + "LibraryEntity"."ownerId" AS "LibraryEntity_ownerId", + "LibraryEntity"."type" AS "LibraryEntity_type", + "LibraryEntity"."importPaths" AS "LibraryEntity_importPaths", + "LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns", + "LibraryEntity"."createdAt" AS "LibraryEntity_createdAt", + "LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt", + "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", + "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", + "LibraryEntity"."isVisible" AS "LibraryEntity_isVisible", + "LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id", + "LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name", + "LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor", + "LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin", + "LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email", + "LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel", + "LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath", + "LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId", + "LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath", + "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", + "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", + "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", + "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", + "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled" +FROM + "libraries" "LibraryEntity" + LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" + AND ( + "LibraryEntity__LibraryEntity_owner"."deletedAt" IS NULL + ) +WHERE + ( + ( + "LibraryEntity"."ownerId" = $1 + AND "LibraryEntity"."isVisible" = $2 + ) + ) + AND ("LibraryEntity"."deletedAt" IS NULL) +ORDER BY + "LibraryEntity"."createdAt" ASC + +-- LibraryRepository.getAll +SELECT + "LibraryEntity"."id" AS "LibraryEntity_id", + "LibraryEntity"."name" AS "LibraryEntity_name", + "LibraryEntity"."ownerId" AS "LibraryEntity_ownerId", + "LibraryEntity"."type" AS "LibraryEntity_type", + "LibraryEntity"."importPaths" AS "LibraryEntity_importPaths", + "LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns", + "LibraryEntity"."createdAt" AS "LibraryEntity_createdAt", + "LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt", + "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", + "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", + "LibraryEntity"."isVisible" AS "LibraryEntity_isVisible", + "LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id", + "LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name", + "LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor", + "LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin", + "LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email", + "LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel", + "LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath", + "LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId", + "LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath", + "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", + "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", + "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", + "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", + "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled" +FROM + "libraries" "LibraryEntity" + LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" + AND ( + "LibraryEntity__LibraryEntity_owner"."deletedAt" IS NULL + ) +WHERE + "LibraryEntity"."deletedAt" IS NULL +ORDER BY + "LibraryEntity"."createdAt" ASC + +-- LibraryRepository.getAllDeleted +SELECT + "LibraryEntity"."id" AS "LibraryEntity_id", + "LibraryEntity"."name" AS "LibraryEntity_name", + "LibraryEntity"."ownerId" AS "LibraryEntity_ownerId", + "LibraryEntity"."type" AS "LibraryEntity_type", + "LibraryEntity"."importPaths" AS "LibraryEntity_importPaths", + "LibraryEntity"."exclusionPatterns" AS "LibraryEntity_exclusionPatterns", + "LibraryEntity"."createdAt" AS "LibraryEntity_createdAt", + "LibraryEntity"."updatedAt" AS "LibraryEntity_updatedAt", + "LibraryEntity"."deletedAt" AS "LibraryEntity_deletedAt", + "LibraryEntity"."refreshedAt" AS "LibraryEntity_refreshedAt", + "LibraryEntity"."isVisible" AS "LibraryEntity_isVisible", + "LibraryEntity__LibraryEntity_owner"."id" AS "LibraryEntity__LibraryEntity_owner_id", + "LibraryEntity__LibraryEntity_owner"."name" AS "LibraryEntity__LibraryEntity_owner_name", + "LibraryEntity__LibraryEntity_owner"."avatarColor" AS "LibraryEntity__LibraryEntity_owner_avatarColor", + "LibraryEntity__LibraryEntity_owner"."isAdmin" AS "LibraryEntity__LibraryEntity_owner_isAdmin", + "LibraryEntity__LibraryEntity_owner"."email" AS "LibraryEntity__LibraryEntity_owner_email", + "LibraryEntity__LibraryEntity_owner"."storageLabel" AS "LibraryEntity__LibraryEntity_owner_storageLabel", + "LibraryEntity__LibraryEntity_owner"."externalPath" AS "LibraryEntity__LibraryEntity_owner_externalPath", + "LibraryEntity__LibraryEntity_owner"."oauthId" AS "LibraryEntity__LibraryEntity_owner_oauthId", + "LibraryEntity__LibraryEntity_owner"."profileImagePath" AS "LibraryEntity__LibraryEntity_owner_profileImagePath", + "LibraryEntity__LibraryEntity_owner"."shouldChangePassword" AS "LibraryEntity__LibraryEntity_owner_shouldChangePassword", + "LibraryEntity__LibraryEntity_owner"."createdAt" AS "LibraryEntity__LibraryEntity_owner_createdAt", + "LibraryEntity__LibraryEntity_owner"."deletedAt" AS "LibraryEntity__LibraryEntity_owner_deletedAt", + "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", + "LibraryEntity__LibraryEntity_owner"."memoriesEnabled" AS "LibraryEntity__LibraryEntity_owner_memoriesEnabled" +FROM + "libraries" "LibraryEntity" + LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" +WHERE + ( + "LibraryEntity"."isVisible" = $1 + AND NOT ("LibraryEntity"."deletedAt" IS NULL) + ) +ORDER BY + "LibraryEntity"."createdAt" ASC + +-- LibraryRepository.getStatistics +SELECT + "libraries"."id" AS "libraries_id", + "libraries"."name" AS "libraries_name", + "libraries"."ownerId" AS "libraries_ownerId", + "libraries"."type" AS "libraries_type", + "libraries"."importPaths" AS "libraries_importPaths", + "libraries"."exclusionPatterns" AS "libraries_exclusionPatterns", + "libraries"."createdAt" AS "libraries_createdAt", + "libraries"."updatedAt" AS "libraries_updatedAt", + "libraries"."deletedAt" AS "libraries_deletedAt", + "libraries"."refreshedAt" AS "libraries_refreshedAt", + "libraries"."isVisible" AS "libraries_isVisible", + COUNT("assets"."id") FILTER ( + WHERE + "assets"."type" = 'IMAGE' + AND "assets"."isVisible" + ) AS "photos", + COUNT("assets"."id") FILTER ( + WHERE + "assets"."type" = 'VIDEO' + AND "assets"."isVisible" + ) AS "videos", + COALESCE(SUM("exif"."fileSizeInByte"), 0) AS "usage" +FROM + "libraries" "libraries" + LEFT JOIN "assets" "assets" ON "assets"."libraryId" = "libraries"."id" + AND ("assets"."deletedAt" IS NULL) + LEFT JOIN "exif" "exif" ON "exif"."assetId" = "assets"."id" +WHERE + ("libraries"."id" = $1) + AND ("libraries"."deletedAt" IS NULL) +GROUP BY + "libraries"."id" + +-- LibraryRepository.getOnlineAssetPaths +SELECT + "assets"."originalPath" AS "assets_originalPath" +FROM + "libraries" "library" + INNER JOIN "assets" "assets" ON "assets"."libraryId" = "library"."id" + AND ("assets"."deletedAt" IS NULL) +WHERE + ( + "library"."id" = $1 + AND "assets"."isOffline" = false + ) + AND ("library"."deletedAt" IS NULL) + +-- LibraryRepository.getAssetIds +SELECT + "assets"."id" AS "assets_id" +FROM + "libraries" "library" + INNER JOIN "assets" "assets" ON "assets"."libraryId" = "library"."id" + AND ("assets"."deletedAt" IS NULL) +WHERE + ("library"."id" = $1) + AND ("library"."deletedAt" IS NULL) diff --git a/server/src/infra/sql/move.repository.sql b/server/src/infra/sql/move.repository.sql new file mode 100644 index 000000000..b83175bd3 --- /dev/null +++ b/server/src/infra/sql/move.repository.sql @@ -0,0 +1,18 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- MoveRepository.getByEntity +SELECT + "MoveEntity"."id" AS "MoveEntity_id", + "MoveEntity"."entityId" AS "MoveEntity_entityId", + "MoveEntity"."pathType" AS "MoveEntity_pathType", + "MoveEntity"."oldPath" AS "MoveEntity_oldPath", + "MoveEntity"."newPath" AS "MoveEntity_newPath" +FROM + "move_history" "MoveEntity" +WHERE + ( + "MoveEntity"."entityId" = $1 + AND "MoveEntity"."pathType" = $2 + ) +LIMIT + 1 diff --git a/server/src/infra/sql/partner.repository.sql b/server/src/infra/sql/partner.repository.sql new file mode 100644 index 000000000..21f9f116b --- /dev/null +++ b/server/src/infra/sql/partner.repository.sql @@ -0,0 +1 @@ +-- NOTE: This file is auto generated by ./sql-generator diff --git a/server/src/infra/sql/person.repository.sql b/server/src/infra/sql/person.repository.sql new file mode 100644 index 000000000..933283746 --- /dev/null +++ b/server/src/infra/sql/person.repository.sql @@ -0,0 +1,367 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- PersonRepository.reassignFaces +UPDATE "asset_faces" +SET + "personId" = $1 +WHERE + "personId" = $2 + +-- PersonRepository.getAllFaces +SELECT + "AssetFaceEntity"."id" AS "AssetFaceEntity_id", + "AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId", + "AssetFaceEntity"."personId" AS "AssetFaceEntity_personId", + "AssetFaceEntity"."embedding" AS "AssetFaceEntity_embedding", + "AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth", + "AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight", + "AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1", + "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", + "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", + "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", + "AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id", + "AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId", + "AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId", + "AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId", + "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", + "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", + "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", + "AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath", + "AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath", + "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", + "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", + "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", + "AssetFaceEntity__AssetFaceEntity_asset"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_updatedAt", + "AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_deletedAt", + "AssetFaceEntity__AssetFaceEntity_asset"."fileCreatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileCreatedAt", + "AssetFaceEntity__AssetFaceEntity_asset"."localDateTime" AS "AssetFaceEntity__AssetFaceEntity_asset_localDateTime", + "AssetFaceEntity__AssetFaceEntity_asset"."fileModifiedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileModifiedAt", + "AssetFaceEntity__AssetFaceEntity_asset"."isFavorite" AS "AssetFaceEntity__AssetFaceEntity_asset_isFavorite", + "AssetFaceEntity__AssetFaceEntity_asset"."isArchived" AS "AssetFaceEntity__AssetFaceEntity_asset_isArchived", + "AssetFaceEntity__AssetFaceEntity_asset"."isExternal" AS "AssetFaceEntity__AssetFaceEntity_asset_isExternal", + "AssetFaceEntity__AssetFaceEntity_asset"."isReadOnly" AS "AssetFaceEntity__AssetFaceEntity_asset_isReadOnly", + "AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline", + "AssetFaceEntity__AssetFaceEntity_asset"."checksum" AS "AssetFaceEntity__AssetFaceEntity_asset_checksum", + "AssetFaceEntity__AssetFaceEntity_asset"."duration" AS "AssetFaceEntity__AssetFaceEntity_asset_duration", + "AssetFaceEntity__AssetFaceEntity_asset"."isVisible" AS "AssetFaceEntity__AssetFaceEntity_asset_isVisible", + "AssetFaceEntity__AssetFaceEntity_asset"."livePhotoVideoId" AS "AssetFaceEntity__AssetFaceEntity_asset_livePhotoVideoId", + "AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName", + "AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath", + "AssetFaceEntity__AssetFaceEntity_asset"."stackParentId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackParentId" +FROM + "asset_faces" "AssetFaceEntity" + LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId" + +-- PersonRepository.getAll +SELECT + "PersonEntity"."id" AS "PersonEntity_id", + "PersonEntity"."createdAt" AS "PersonEntity_createdAt", + "PersonEntity"."updatedAt" AS "PersonEntity_updatedAt", + "PersonEntity"."ownerId" AS "PersonEntity_ownerId", + "PersonEntity"."name" AS "PersonEntity_name", + "PersonEntity"."birthDate" AS "PersonEntity_birthDate", + "PersonEntity"."thumbnailPath" AS "PersonEntity_thumbnailPath", + "PersonEntity"."faceAssetId" AS "PersonEntity_faceAssetId", + "PersonEntity"."isHidden" AS "PersonEntity_isHidden" +FROM + "person" "PersonEntity" + +-- PersonRepository.getAllWithoutThumbnail +SELECT + "PersonEntity"."id" AS "PersonEntity_id", + "PersonEntity"."createdAt" AS "PersonEntity_createdAt", + "PersonEntity"."updatedAt" AS "PersonEntity_updatedAt", + "PersonEntity"."ownerId" AS "PersonEntity_ownerId", + "PersonEntity"."name" AS "PersonEntity_name", + "PersonEntity"."birthDate" AS "PersonEntity_birthDate", + "PersonEntity"."thumbnailPath" AS "PersonEntity_thumbnailPath", + "PersonEntity"."faceAssetId" AS "PersonEntity_faceAssetId", + "PersonEntity"."isHidden" AS "PersonEntity_isHidden" +FROM + "person" "PersonEntity" +WHERE + ("PersonEntity"."thumbnailPath" = $1) + +-- PersonRepository.getAllForUser +SELECT + "person"."id" AS "person_id", + "person"."createdAt" AS "person_createdAt", + "person"."updatedAt" AS "person_updatedAt", + "person"."ownerId" AS "person_ownerId", + "person"."name" AS "person_name", + "person"."birthDate" AS "person_birthDate", + "person"."thumbnailPath" AS "person_thumbnailPath", + "person"."faceAssetId" AS "person_faceAssetId", + "person"."isHidden" AS "person_isHidden" +FROM + "person" "person" + LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id" + INNER JOIN "assets" "asset" ON "asset"."id" = "face"."assetId" + AND ("asset"."deletedAt" IS NULL) +WHERE + "person"."ownerId" = $1 + AND "person"."isHidden" = false +GROUP BY + "person"."id" +HAVING + "person"."name" != '' + OR COUNT("face"."assetId") >= $2 +ORDER BY + "person"."isHidden" ASC, + NULLIF("person"."name", '') IS NULL ASC, + COUNT("face"."assetId") DESC, + NULLIF("person"."name", '') ASC NULLS LAST +LIMIT + 500 + +-- PersonRepository.getAllWithoutFaces +SELECT + "person"."id" AS "person_id", + "person"."createdAt" AS "person_createdAt", + "person"."updatedAt" AS "person_updatedAt", + "person"."ownerId" AS "person_ownerId", + "person"."name" AS "person_name", + "person"."birthDate" AS "person_birthDate", + "person"."thumbnailPath" AS "person_thumbnailPath", + "person"."faceAssetId" AS "person_faceAssetId", + "person"."isHidden" AS "person_isHidden" +FROM + "person" "person" + LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id" +GROUP BY + "person"."id" +HAVING + COUNT("face"."assetId") = 0 + +-- PersonRepository.getById +SELECT + "PersonEntity"."id" AS "PersonEntity_id", + "PersonEntity"."createdAt" AS "PersonEntity_createdAt", + "PersonEntity"."updatedAt" AS "PersonEntity_updatedAt", + "PersonEntity"."ownerId" AS "PersonEntity_ownerId", + "PersonEntity"."name" AS "PersonEntity_name", + "PersonEntity"."birthDate" AS "PersonEntity_birthDate", + "PersonEntity"."thumbnailPath" AS "PersonEntity_thumbnailPath", + "PersonEntity"."faceAssetId" AS "PersonEntity_faceAssetId", + "PersonEntity"."isHidden" AS "PersonEntity_isHidden" +FROM + "person" "PersonEntity" +WHERE + ("PersonEntity"."id" = $1) +LIMIT + 1 + +-- PersonRepository.getByName +SELECT + "person"."id" AS "person_id", + "person"."createdAt" AS "person_createdAt", + "person"."updatedAt" AS "person_updatedAt", + "person"."ownerId" AS "person_ownerId", + "person"."name" AS "person_name", + "person"."birthDate" AS "person_birthDate", + "person"."thumbnailPath" AS "person_thumbnailPath", + "person"."faceAssetId" AS "person_faceAssetId", + "person"."isHidden" AS "person_isHidden" +FROM + "person" "person" + LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id" +WHERE + "person"."ownerId" = $1 + AND ( + LOWER("person"."name") LIKE $2 + OR LOWER("person"."name") LIKE $3 + ) +GROUP BY + "person"."id" +ORDER BY + COUNT("face"."assetId") DESC +LIMIT + 20 + +-- PersonRepository.getStatistics +SELECT DISTINCT + COUNT(DISTINCT ("face"."id")) AS "cnt" +FROM + "asset_faces" "face" + LEFT JOIN "assets" "asset" ON "asset"."id" = "face"."assetId" + AND ("asset"."deletedAt" IS NULL) +WHERE + "face"."personId" = $1 + AND "asset"."isArchived" = false + AND "asset"."deletedAt" IS NULL + AND "asset"."livePhotoVideoId" IS NULL + +-- PersonRepository.getAssets +SELECT DISTINCT + "distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id", + "distinctAlias"."AssetEntity_fileCreatedAt" +FROM + ( + SELECT + "AssetEntity"."id" AS "AssetEntity_id", + "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", + "AssetEntity"."ownerId" AS "AssetEntity_ownerId", + "AssetEntity"."libraryId" AS "AssetEntity_libraryId", + "AssetEntity"."deviceId" AS "AssetEntity_deviceId", + "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."originalPath" AS "AssetEntity_originalPath", + "AssetEntity"."resizePath" AS "AssetEntity_resizePath", + "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", + "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", + "AssetEntity"."createdAt" AS "AssetEntity_createdAt", + "AssetEntity"."updatedAt" AS "AssetEntity_updatedAt", + "AssetEntity"."deletedAt" AS "AssetEntity_deletedAt", + "AssetEntity"."fileCreatedAt" AS "AssetEntity_fileCreatedAt", + "AssetEntity"."localDateTime" AS "AssetEntity_localDateTime", + "AssetEntity"."fileModifiedAt" AS "AssetEntity_fileModifiedAt", + "AssetEntity"."isFavorite" AS "AssetEntity_isFavorite", + "AssetEntity"."isArchived" AS "AssetEntity_isArchived", + "AssetEntity"."isExternal" AS "AssetEntity_isExternal", + "AssetEntity"."isReadOnly" AS "AssetEntity_isReadOnly", + "AssetEntity"."isOffline" AS "AssetEntity_isOffline", + "AssetEntity"."checksum" AS "AssetEntity_checksum", + "AssetEntity"."duration" AS "AssetEntity_duration", + "AssetEntity"."isVisible" AS "AssetEntity_isVisible", + "AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId", + "AssetEntity"."originalFileName" AS "AssetEntity_originalFileName", + "AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath", + "AssetEntity"."stackParentId" AS "AssetEntity_stackParentId", + "AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id", + "AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId", + "AssetEntity__AssetEntity_faces"."personId" AS "AssetEntity__AssetEntity_faces_personId", + "AssetEntity__AssetEntity_faces"."embedding" AS "AssetEntity__AssetEntity_faces_embedding", + "AssetEntity__AssetEntity_faces"."imageWidth" AS "AssetEntity__AssetEntity_faces_imageWidth", + "AssetEntity__AssetEntity_faces"."imageHeight" AS "AssetEntity__AssetEntity_faces_imageHeight", + "AssetEntity__AssetEntity_faces"."boundingBoxX1" AS "AssetEntity__AssetEntity_faces_boundingBoxX1", + "AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1", + "AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2", + "AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."ownerId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_ownerId", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."name" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_name", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."birthDate" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_birthDate", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."thumbnailPath" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_thumbnailPath", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId", + "8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden", + "AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId", + "AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description", + "AssetEntity__AssetEntity_exifInfo"."exifImageWidth" AS "AssetEntity__AssetEntity_exifInfo_exifImageWidth", + "AssetEntity__AssetEntity_exifInfo"."exifImageHeight" AS "AssetEntity__AssetEntity_exifInfo_exifImageHeight", + "AssetEntity__AssetEntity_exifInfo"."fileSizeInByte" AS "AssetEntity__AssetEntity_exifInfo_fileSizeInByte", + "AssetEntity__AssetEntity_exifInfo"."orientation" AS "AssetEntity__AssetEntity_exifInfo_orientation", + "AssetEntity__AssetEntity_exifInfo"."dateTimeOriginal" AS "AssetEntity__AssetEntity_exifInfo_dateTimeOriginal", + "AssetEntity__AssetEntity_exifInfo"."modifyDate" AS "AssetEntity__AssetEntity_exifInfo_modifyDate", + "AssetEntity__AssetEntity_exifInfo"."timeZone" AS "AssetEntity__AssetEntity_exifInfo_timeZone", + "AssetEntity__AssetEntity_exifInfo"."latitude" AS "AssetEntity__AssetEntity_exifInfo_latitude", + "AssetEntity__AssetEntity_exifInfo"."longitude" AS "AssetEntity__AssetEntity_exifInfo_longitude", + "AssetEntity__AssetEntity_exifInfo"."projectionType" AS "AssetEntity__AssetEntity_exifInfo_projectionType", + "AssetEntity__AssetEntity_exifInfo"."city" AS "AssetEntity__AssetEntity_exifInfo_city", + "AssetEntity__AssetEntity_exifInfo"."livePhotoCID" AS "AssetEntity__AssetEntity_exifInfo_livePhotoCID", + "AssetEntity__AssetEntity_exifInfo"."state" AS "AssetEntity__AssetEntity_exifInfo_state", + "AssetEntity__AssetEntity_exifInfo"."country" AS "AssetEntity__AssetEntity_exifInfo_country", + "AssetEntity__AssetEntity_exifInfo"."make" AS "AssetEntity__AssetEntity_exifInfo_make", + "AssetEntity__AssetEntity_exifInfo"."model" AS "AssetEntity__AssetEntity_exifInfo_model", + "AssetEntity__AssetEntity_exifInfo"."lensModel" AS "AssetEntity__AssetEntity_exifInfo_lensModel", + "AssetEntity__AssetEntity_exifInfo"."fNumber" AS "AssetEntity__AssetEntity_exifInfo_fNumber", + "AssetEntity__AssetEntity_exifInfo"."focalLength" AS "AssetEntity__AssetEntity_exifInfo_focalLength", + "AssetEntity__AssetEntity_exifInfo"."iso" AS "AssetEntity__AssetEntity_exifInfo_iso", + "AssetEntity__AssetEntity_exifInfo"."exposureTime" AS "AssetEntity__AssetEntity_exifInfo_exposureTime", + "AssetEntity__AssetEntity_exifInfo"."profileDescription" AS "AssetEntity__AssetEntity_exifInfo_profileDescription", + "AssetEntity__AssetEntity_exifInfo"."colorspace" AS "AssetEntity__AssetEntity_exifInfo_colorspace", + "AssetEntity__AssetEntity_exifInfo"."bitsPerSample" AS "AssetEntity__AssetEntity_exifInfo_bitsPerSample", + "AssetEntity__AssetEntity_exifInfo"."fps" AS "AssetEntity__AssetEntity_exifInfo_fps", + "AssetEntity__AssetEntity_exifInfo"."exifTextSearchableColumn" AS "AssetEntity__AssetEntity_exifInfo_exifTextSearchableColumn" + FROM + "assets" "AssetEntity" + LEFT JOIN "asset_faces" "AssetEntity__AssetEntity_faces" ON "AssetEntity__AssetEntity_faces"."assetId" = "AssetEntity"."id" + LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId" + LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id" + WHERE + ( + ( + "AssetEntity__AssetEntity_faces"."personId" = $1 + AND "AssetEntity"."isVisible" = $2 + AND "AssetEntity"."isArchived" = $3 + ) + ) + AND ("AssetEntity"."deletedAt" IS NULL) + ) "distinctAlias" +ORDER BY + "distinctAlias"."AssetEntity_fileCreatedAt" DESC, + "AssetEntity_id" ASC +LIMIT + 1000 + +-- PersonRepository.getFacesByIds +SELECT + "AssetFaceEntity"."id" AS "AssetFaceEntity_id", + "AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId", + "AssetFaceEntity"."personId" AS "AssetFaceEntity_personId", + "AssetFaceEntity"."embedding" AS "AssetFaceEntity_embedding", + "AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth", + "AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight", + "AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1", + "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", + "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", + "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2", + "AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id", + "AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId", + "AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId", + "AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId", + "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", + "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", + "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", + "AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath", + "AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath", + "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", + "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", + "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", + "AssetFaceEntity__AssetFaceEntity_asset"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_updatedAt", + "AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_deletedAt", + "AssetFaceEntity__AssetFaceEntity_asset"."fileCreatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileCreatedAt", + "AssetFaceEntity__AssetFaceEntity_asset"."localDateTime" AS "AssetFaceEntity__AssetFaceEntity_asset_localDateTime", + "AssetFaceEntity__AssetFaceEntity_asset"."fileModifiedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileModifiedAt", + "AssetFaceEntity__AssetFaceEntity_asset"."isFavorite" AS "AssetFaceEntity__AssetFaceEntity_asset_isFavorite", + "AssetFaceEntity__AssetFaceEntity_asset"."isArchived" AS "AssetFaceEntity__AssetFaceEntity_asset_isArchived", + "AssetFaceEntity__AssetFaceEntity_asset"."isExternal" AS "AssetFaceEntity__AssetFaceEntity_asset_isExternal", + "AssetFaceEntity__AssetFaceEntity_asset"."isReadOnly" AS "AssetFaceEntity__AssetFaceEntity_asset_isReadOnly", + "AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline", + "AssetFaceEntity__AssetFaceEntity_asset"."checksum" AS "AssetFaceEntity__AssetFaceEntity_asset_checksum", + "AssetFaceEntity__AssetFaceEntity_asset"."duration" AS "AssetFaceEntity__AssetFaceEntity_asset_duration", + "AssetFaceEntity__AssetFaceEntity_asset"."isVisible" AS "AssetFaceEntity__AssetFaceEntity_asset_isVisible", + "AssetFaceEntity__AssetFaceEntity_asset"."livePhotoVideoId" AS "AssetFaceEntity__AssetFaceEntity_asset_livePhotoVideoId", + "AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName", + "AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath", + "AssetFaceEntity__AssetFaceEntity_asset"."stackParentId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackParentId" +FROM + "asset_faces" "AssetFaceEntity" + LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId" +WHERE + ( + ( + "AssetFaceEntity"."assetId" = $1 + AND "AssetFaceEntity"."personId" = $2 + ) + ) + +-- PersonRepository.getRandomFace +SELECT + "AssetFaceEntity"."id" AS "AssetFaceEntity_id", + "AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId", + "AssetFaceEntity"."personId" AS "AssetFaceEntity_personId", + "AssetFaceEntity"."embedding" AS "AssetFaceEntity_embedding", + "AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth", + "AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight", + "AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1", + "AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1", + "AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2", + "AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2" +FROM + "asset_faces" "AssetFaceEntity" +WHERE + ("AssetFaceEntity"."personId" = $1) +LIMIT + 1 diff --git a/server/src/infra/sql/shared.link.repository.sql b/server/src/infra/sql/shared.link.repository.sql new file mode 100644 index 000000000..ffb05cd39 --- /dev/null +++ b/server/src/infra/sql/shared.link.repository.sql @@ -0,0 +1,327 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- SharedLinkRepository.get +SELECT DISTINCT + "distinctAlias"."SharedLinkEntity_id" AS "ids_SharedLinkEntity_id", + "distinctAlias"."SharedLinkEntity_createdAt", + "distinctAlias"."SharedLinkEntity__SharedLinkEntity_assets_fileCreatedAt", + "distinctAlias"."4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_fileCreatedAt" +FROM + ( + SELECT + "SharedLinkEntity"."id" AS "SharedLinkEntity_id", + "SharedLinkEntity"."description" AS "SharedLinkEntity_description", + "SharedLinkEntity"."password" AS "SharedLinkEntity_password", + "SharedLinkEntity"."userId" AS "SharedLinkEntity_userId", + "SharedLinkEntity"."key" AS "SharedLinkEntity_key", + "SharedLinkEntity"."type" AS "SharedLinkEntity_type", + "SharedLinkEntity"."createdAt" AS "SharedLinkEntity_createdAt", + "SharedLinkEntity"."expiresAt" AS "SharedLinkEntity_expiresAt", + "SharedLinkEntity"."allowUpload" AS "SharedLinkEntity_allowUpload", + "SharedLinkEntity"."allowDownload" AS "SharedLinkEntity_allowDownload", + "SharedLinkEntity"."showExif" AS "SharedLinkEntity_showExif", + "SharedLinkEntity"."albumId" AS "SharedLinkEntity_albumId", + "SharedLinkEntity__SharedLinkEntity_assets"."id" AS "SharedLinkEntity__SharedLinkEntity_assets_id", + "SharedLinkEntity__SharedLinkEntity_assets"."deviceAssetId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceAssetId", + "SharedLinkEntity__SharedLinkEntity_assets"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_assets_ownerId", + "SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId", + "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", + "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", + "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", + "SharedLinkEntity__SharedLinkEntity_assets"."resizePath" AS "SharedLinkEntity__SharedLinkEntity_assets_resizePath", + "SharedLinkEntity__SharedLinkEntity_assets"."webpPath" AS "SharedLinkEntity__SharedLinkEntity_assets_webpPath", + "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", + "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", + "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", + "SharedLinkEntity__SharedLinkEntity_assets"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_updatedAt", + "SharedLinkEntity__SharedLinkEntity_assets"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_deletedAt", + "SharedLinkEntity__SharedLinkEntity_assets"."fileCreatedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_fileCreatedAt", + "SharedLinkEntity__SharedLinkEntity_assets"."localDateTime" AS "SharedLinkEntity__SharedLinkEntity_assets_localDateTime", + "SharedLinkEntity__SharedLinkEntity_assets"."fileModifiedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_fileModifiedAt", + "SharedLinkEntity__SharedLinkEntity_assets"."isFavorite" AS "SharedLinkEntity__SharedLinkEntity_assets_isFavorite", + "SharedLinkEntity__SharedLinkEntity_assets"."isArchived" AS "SharedLinkEntity__SharedLinkEntity_assets_isArchived", + "SharedLinkEntity__SharedLinkEntity_assets"."isExternal" AS "SharedLinkEntity__SharedLinkEntity_assets_isExternal", + "SharedLinkEntity__SharedLinkEntity_assets"."isReadOnly" AS "SharedLinkEntity__SharedLinkEntity_assets_isReadOnly", + "SharedLinkEntity__SharedLinkEntity_assets"."isOffline" AS "SharedLinkEntity__SharedLinkEntity_assets_isOffline", + "SharedLinkEntity__SharedLinkEntity_assets"."checksum" AS "SharedLinkEntity__SharedLinkEntity_assets_checksum", + "SharedLinkEntity__SharedLinkEntity_assets"."duration" AS "SharedLinkEntity__SharedLinkEntity_assets_duration", + "SharedLinkEntity__SharedLinkEntity_assets"."isVisible" AS "SharedLinkEntity__SharedLinkEntity_assets_isVisible", + "SharedLinkEntity__SharedLinkEntity_assets"."livePhotoVideoId" AS "SharedLinkEntity__SharedLinkEntity_assets_livePhotoVideoId", + "SharedLinkEntity__SharedLinkEntity_assets"."originalFileName" AS "SharedLinkEntity__SharedLinkEntity_assets_originalFileName", + "SharedLinkEntity__SharedLinkEntity_assets"."sidecarPath" AS "SharedLinkEntity__SharedLinkEntity_assets_sidecarPath", + "SharedLinkEntity__SharedLinkEntity_assets"."stackParentId" AS "SharedLinkEntity__SharedLinkEntity_assets_stackParentId", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."assetId" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_assetId", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."description" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_description", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."exifImageWidth" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_exifImageWidth", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."exifImageHeight" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_exifImageHeight", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."fileSizeInByte" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_fileSizeInByte", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."orientation" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_orientation", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."dateTimeOriginal" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_dateTimeOriginal", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."modifyDate" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_modifyDate", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."timeZone" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_timeZone", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."latitude" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_latitude", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."longitude" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_longitude", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."projectionType" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_projectionType", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."city" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_city", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."livePhotoCID" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_livePhotoCID", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."state" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_state", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."country" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_country", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."make" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_make", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."model" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_model", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."lensModel" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_lensModel", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."fNumber" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_fNumber", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."focalLength" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_focalLength", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."iso" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_iso", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."exposureTime" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_exposureTime", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."profileDescription" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_profileDescription", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."colorspace" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_colorspace", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."bitsPerSample" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_bitsPerSample", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."fps" AS "9b1d35b344d838023994a3233afd6ffe098be6d8_fps", + "9b1d35b344d838023994a3233afd6ffe098be6d8"."exifTextSearchableColumn" AS "e18de9deffa83f81ac3c43b5e8c2f08dba727bf8", + "SharedLinkEntity__SharedLinkEntity_album"."id" AS "SharedLinkEntity__SharedLinkEntity_album_id", + "SharedLinkEntity__SharedLinkEntity_album"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_album_ownerId", + "SharedLinkEntity__SharedLinkEntity_album"."albumName" AS "SharedLinkEntity__SharedLinkEntity_album_albumName", + "SharedLinkEntity__SharedLinkEntity_album"."description" AS "SharedLinkEntity__SharedLinkEntity_album_description", + "SharedLinkEntity__SharedLinkEntity_album"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_album_createdAt", + "SharedLinkEntity__SharedLinkEntity_album"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_album_updatedAt", + "SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt", + "SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId", + "SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."id" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_id", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceAssetId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceAssetId", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."ownerId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_ownerId", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."libraryId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_libraryId", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."resizePath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_resizePath", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."webpPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_webpPath", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."createdAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_createdAt", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."updatedAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_updatedAt", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deletedAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deletedAt", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."fileCreatedAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_fileCreatedAt", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."localDateTime" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_localDateTime", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."fileModifiedAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_fileModifiedAt", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isFavorite" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isFavorite", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isArchived" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isArchived", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isExternal" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isExternal", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isReadOnly" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isReadOnly", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isOffline" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isOffline", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."checksum" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_checksum", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."duration" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_duration", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."isVisible" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_isVisible", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."livePhotoVideoId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_livePhotoVideoId", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalFileName" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalFileName", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."sidecarPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_sidecarPath", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."stackParentId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_stackParentId", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."assetId" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_assetId", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."description" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_description", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."exifImageWidth" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_exifImageWidth", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."exifImageHeight" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_exifImageHeight", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."fileSizeInByte" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_fileSizeInByte", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."orientation" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_orientation", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."dateTimeOriginal" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_dateTimeOriginal", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."modifyDate" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_modifyDate", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."timeZone" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_timeZone", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."latitude" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_latitude", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."longitude" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_longitude", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."projectionType" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_projectionType", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."city" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_city", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."livePhotoCID" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_livePhotoCID", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."state" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_state", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."country" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_country", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."make" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_make", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."model" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_model", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."lensModel" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_lensModel", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."fNumber" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_fNumber", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."focalLength" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_focalLength", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."iso" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_iso", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."exposureTime" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_exposureTime", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."profileDescription" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_profileDescription", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."colorspace" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_colorspace", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."bitsPerSample" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_bitsPerSample", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."fps" AS "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f_fps", + "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."exifTextSearchableColumn" AS "96535c8046de591cca9b8c5825e6c5db502b0e6a", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."avatarColor" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_avatarColor", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."externalPath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_externalPath", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."oauthId" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_oauthId", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileImagePath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileImagePath", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled" + FROM + "shared_links" "SharedLinkEntity" + LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id" + LEFT JOIN "assets" "SharedLinkEntity__SharedLinkEntity_assets" ON "SharedLinkEntity__SharedLinkEntity_assets"."id" = "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."assetsId" + AND ( + "SharedLinkEntity__SharedLinkEntity_assets"."deletedAt" IS NULL + ) + LEFT JOIN "exif" "9b1d35b344d838023994a3233afd6ffe098be6d8" ON "9b1d35b344d838023994a3233afd6ffe098be6d8"."assetId" = "SharedLinkEntity__SharedLinkEntity_assets"."id" + LEFT JOIN "albums" "SharedLinkEntity__SharedLinkEntity_album" ON "SharedLinkEntity__SharedLinkEntity_album"."id" = "SharedLinkEntity"."albumId" + AND ( + "SharedLinkEntity__SharedLinkEntity_album"."deletedAt" IS NULL + ) + LEFT JOIN "albums_assets_assets" "760f12c00d97bdcec1ce224d1e3bf449859942b6" ON "760f12c00d97bdcec1ce224d1e3bf449859942b6"."albumsId" = "SharedLinkEntity__SharedLinkEntity_album"."id" + LEFT JOIN "assets" "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6" ON "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."id" = "760f12c00d97bdcec1ce224d1e3bf449859942b6"."assetsId" + AND ( + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deletedAt" IS NULL + ) + LEFT JOIN "exif" "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f" ON "d9f2f4dd8920bad1d6907cdb1d699732daff3c2f"."assetId" = "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."id" + LEFT JOIN "users" "6d7fd45329a05fd86b3dbcacde87fe76e33a422d" ON "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" = "SharedLinkEntity__SharedLinkEntity_album"."ownerId" + AND ( + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" IS NULL + ) + WHERE + ( + "SharedLinkEntity"."id" = $1 + AND "SharedLinkEntity"."userId" = $2 + ) + ) "distinctAlias" +ORDER BY + "distinctAlias"."SharedLinkEntity_createdAt" DESC, + "distinctAlias"."SharedLinkEntity__SharedLinkEntity_assets_fileCreatedAt" ASC, + "distinctAlias"."4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_fileCreatedAt" ASC, + "SharedLinkEntity_id" ASC +LIMIT + 1 + +-- SharedLinkRepository.getAll +SELECT + "SharedLinkEntity"."id" AS "SharedLinkEntity_id", + "SharedLinkEntity"."description" AS "SharedLinkEntity_description", + "SharedLinkEntity"."password" AS "SharedLinkEntity_password", + "SharedLinkEntity"."userId" AS "SharedLinkEntity_userId", + "SharedLinkEntity"."key" AS "SharedLinkEntity_key", + "SharedLinkEntity"."type" AS "SharedLinkEntity_type", + "SharedLinkEntity"."createdAt" AS "SharedLinkEntity_createdAt", + "SharedLinkEntity"."expiresAt" AS "SharedLinkEntity_expiresAt", + "SharedLinkEntity"."allowUpload" AS "SharedLinkEntity_allowUpload", + "SharedLinkEntity"."allowDownload" AS "SharedLinkEntity_allowDownload", + "SharedLinkEntity"."showExif" AS "SharedLinkEntity_showExif", + "SharedLinkEntity"."albumId" AS "SharedLinkEntity_albumId", + "SharedLinkEntity__SharedLinkEntity_assets"."id" AS "SharedLinkEntity__SharedLinkEntity_assets_id", + "SharedLinkEntity__SharedLinkEntity_assets"."deviceAssetId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceAssetId", + "SharedLinkEntity__SharedLinkEntity_assets"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_assets_ownerId", + "SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId", + "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", + "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", + "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", + "SharedLinkEntity__SharedLinkEntity_assets"."resizePath" AS "SharedLinkEntity__SharedLinkEntity_assets_resizePath", + "SharedLinkEntity__SharedLinkEntity_assets"."webpPath" AS "SharedLinkEntity__SharedLinkEntity_assets_webpPath", + "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", + "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", + "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", + "SharedLinkEntity__SharedLinkEntity_assets"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_updatedAt", + "SharedLinkEntity__SharedLinkEntity_assets"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_deletedAt", + "SharedLinkEntity__SharedLinkEntity_assets"."fileCreatedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_fileCreatedAt", + "SharedLinkEntity__SharedLinkEntity_assets"."localDateTime" AS "SharedLinkEntity__SharedLinkEntity_assets_localDateTime", + "SharedLinkEntity__SharedLinkEntity_assets"."fileModifiedAt" AS "SharedLinkEntity__SharedLinkEntity_assets_fileModifiedAt", + "SharedLinkEntity__SharedLinkEntity_assets"."isFavorite" AS "SharedLinkEntity__SharedLinkEntity_assets_isFavorite", + "SharedLinkEntity__SharedLinkEntity_assets"."isArchived" AS "SharedLinkEntity__SharedLinkEntity_assets_isArchived", + "SharedLinkEntity__SharedLinkEntity_assets"."isExternal" AS "SharedLinkEntity__SharedLinkEntity_assets_isExternal", + "SharedLinkEntity__SharedLinkEntity_assets"."isReadOnly" AS "SharedLinkEntity__SharedLinkEntity_assets_isReadOnly", + "SharedLinkEntity__SharedLinkEntity_assets"."isOffline" AS "SharedLinkEntity__SharedLinkEntity_assets_isOffline", + "SharedLinkEntity__SharedLinkEntity_assets"."checksum" AS "SharedLinkEntity__SharedLinkEntity_assets_checksum", + "SharedLinkEntity__SharedLinkEntity_assets"."duration" AS "SharedLinkEntity__SharedLinkEntity_assets_duration", + "SharedLinkEntity__SharedLinkEntity_assets"."isVisible" AS "SharedLinkEntity__SharedLinkEntity_assets_isVisible", + "SharedLinkEntity__SharedLinkEntity_assets"."livePhotoVideoId" AS "SharedLinkEntity__SharedLinkEntity_assets_livePhotoVideoId", + "SharedLinkEntity__SharedLinkEntity_assets"."originalFileName" AS "SharedLinkEntity__SharedLinkEntity_assets_originalFileName", + "SharedLinkEntity__SharedLinkEntity_assets"."sidecarPath" AS "SharedLinkEntity__SharedLinkEntity_assets_sidecarPath", + "SharedLinkEntity__SharedLinkEntity_assets"."stackParentId" AS "SharedLinkEntity__SharedLinkEntity_assets_stackParentId", + "SharedLinkEntity__SharedLinkEntity_album"."id" AS "SharedLinkEntity__SharedLinkEntity_album_id", + "SharedLinkEntity__SharedLinkEntity_album"."ownerId" AS "SharedLinkEntity__SharedLinkEntity_album_ownerId", + "SharedLinkEntity__SharedLinkEntity_album"."albumName" AS "SharedLinkEntity__SharedLinkEntity_album_albumName", + "SharedLinkEntity__SharedLinkEntity_album"."description" AS "SharedLinkEntity__SharedLinkEntity_album_description", + "SharedLinkEntity__SharedLinkEntity_album"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_album_createdAt", + "SharedLinkEntity__SharedLinkEntity_album"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_album_updatedAt", + "SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt", + "SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId", + "SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."avatarColor" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_avatarColor", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."isAdmin" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_isAdmin", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."email" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_email", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."storageLabel" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_storageLabel", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."externalPath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_externalPath", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."oauthId" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_oauthId", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileImagePath" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileImagePath", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."shouldChangePassword" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_shouldChangePassword", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."createdAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_createdAt", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_deletedAt", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."memoriesEnabled" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_memoriesEnabled" +FROM + "shared_links" "SharedLinkEntity" + LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id" + LEFT JOIN "assets" "SharedLinkEntity__SharedLinkEntity_assets" ON "SharedLinkEntity__SharedLinkEntity_assets"."id" = "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."assetsId" + AND ( + "SharedLinkEntity__SharedLinkEntity_assets"."deletedAt" IS NULL + ) + LEFT JOIN "albums" "SharedLinkEntity__SharedLinkEntity_album" ON "SharedLinkEntity__SharedLinkEntity_album"."id" = "SharedLinkEntity"."albumId" + AND ( + "SharedLinkEntity__SharedLinkEntity_album"."deletedAt" IS NULL + ) + LEFT JOIN "users" "6d7fd45329a05fd86b3dbcacde87fe76e33a422d" ON "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" = "SharedLinkEntity__SharedLinkEntity_album"."ownerId" + AND ( + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."deletedAt" IS NULL + ) +WHERE + ("SharedLinkEntity"."userId" = $1) +ORDER BY + "SharedLinkEntity"."createdAt" DESC + +-- SharedLinkRepository.getByKey +SELECT DISTINCT + "distinctAlias"."SharedLinkEntity_id" AS "ids_SharedLinkEntity_id" +FROM + ( + SELECT + "SharedLinkEntity"."id" AS "SharedLinkEntity_id", + "SharedLinkEntity"."description" AS "SharedLinkEntity_description", + "SharedLinkEntity"."password" AS "SharedLinkEntity_password", + "SharedLinkEntity"."userId" AS "SharedLinkEntity_userId", + "SharedLinkEntity"."key" AS "SharedLinkEntity_key", + "SharedLinkEntity"."type" AS "SharedLinkEntity_type", + "SharedLinkEntity"."createdAt" AS "SharedLinkEntity_createdAt", + "SharedLinkEntity"."expiresAt" AS "SharedLinkEntity_expiresAt", + "SharedLinkEntity"."allowUpload" AS "SharedLinkEntity_allowUpload", + "SharedLinkEntity"."allowDownload" AS "SharedLinkEntity_allowDownload", + "SharedLinkEntity"."showExif" AS "SharedLinkEntity_showExif", + "SharedLinkEntity"."albumId" AS "SharedLinkEntity_albumId", + "SharedLinkEntity__SharedLinkEntity_user"."id" AS "SharedLinkEntity__SharedLinkEntity_user_id", + "SharedLinkEntity__SharedLinkEntity_user"."name" AS "SharedLinkEntity__SharedLinkEntity_user_name", + "SharedLinkEntity__SharedLinkEntity_user"."avatarColor" AS "SharedLinkEntity__SharedLinkEntity_user_avatarColor", + "SharedLinkEntity__SharedLinkEntity_user"."isAdmin" AS "SharedLinkEntity__SharedLinkEntity_user_isAdmin", + "SharedLinkEntity__SharedLinkEntity_user"."email" AS "SharedLinkEntity__SharedLinkEntity_user_email", + "SharedLinkEntity__SharedLinkEntity_user"."storageLabel" AS "SharedLinkEntity__SharedLinkEntity_user_storageLabel", + "SharedLinkEntity__SharedLinkEntity_user"."externalPath" AS "SharedLinkEntity__SharedLinkEntity_user_externalPath", + "SharedLinkEntity__SharedLinkEntity_user"."oauthId" AS "SharedLinkEntity__SharedLinkEntity_user_oauthId", + "SharedLinkEntity__SharedLinkEntity_user"."profileImagePath" AS "SharedLinkEntity__SharedLinkEntity_user_profileImagePath", + "SharedLinkEntity__SharedLinkEntity_user"."shouldChangePassword" AS "SharedLinkEntity__SharedLinkEntity_user_shouldChangePassword", + "SharedLinkEntity__SharedLinkEntity_user"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_user_createdAt", + "SharedLinkEntity__SharedLinkEntity_user"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_user_deletedAt", + "SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt", + "SharedLinkEntity__SharedLinkEntity_user"."memoriesEnabled" AS "SharedLinkEntity__SharedLinkEntity_user_memoriesEnabled" + FROM + "shared_links" "SharedLinkEntity" + LEFT JOIN "users" "SharedLinkEntity__SharedLinkEntity_user" ON "SharedLinkEntity__SharedLinkEntity_user"."id" = "SharedLinkEntity"."userId" + AND ( + "SharedLinkEntity__SharedLinkEntity_user"."deletedAt" IS NULL + ) + WHERE + ("SharedLinkEntity"."key" = $1) + ) "distinctAlias" +ORDER BY + "SharedLinkEntity_id" ASC +LIMIT + 1 diff --git a/server/src/infra/sql/system.config.repository.sql b/server/src/infra/sql/system.config.repository.sql new file mode 100644 index 000000000..276cab20f --- /dev/null +++ b/server/src/infra/sql/system.config.repository.sql @@ -0,0 +1,13 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- SystemConfigRepository.load +SELECT + "SystemConfigEntity"."key" AS "SystemConfigEntity_key", + "SystemConfigEntity"."value" AS "SystemConfigEntity_value" +FROM + "system_config" "SystemConfigEntity" + +-- SystemConfigRepository.deleteKeys +DELETE FROM "system_config" +WHERE + "key" IN ($1, $2, $3, $4, $5, $6, $7, $8, $9) diff --git a/server/src/infra/sql/system.metadata.repository.sql b/server/src/infra/sql/system.metadata.repository.sql new file mode 100644 index 000000000..21f9f116b --- /dev/null +++ b/server/src/infra/sql/system.metadata.repository.sql @@ -0,0 +1 @@ +-- NOTE: This file is auto generated by ./sql-generator diff --git a/server/src/infra/sql/tag.repository.sql b/server/src/infra/sql/tag.repository.sql new file mode 100644 index 000000000..21f9f116b --- /dev/null +++ b/server/src/infra/sql/tag.repository.sql @@ -0,0 +1 @@ +-- NOTE: This file is auto generated by ./sql-generator diff --git a/server/src/infra/sql/user.repository.sql b/server/src/infra/sql/user.repository.sql new file mode 100644 index 000000000..d21f1e7b0 --- /dev/null +++ b/server/src/infra/sql/user.repository.sql @@ -0,0 +1,143 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- UserRepository.getAdmin +SELECT + "UserEntity"."id" AS "UserEntity_id", + "UserEntity"."name" AS "UserEntity_name", + "UserEntity"."avatarColor" AS "UserEntity_avatarColor", + "UserEntity"."isAdmin" AS "UserEntity_isAdmin", + "UserEntity"."email" AS "UserEntity_email", + "UserEntity"."storageLabel" AS "UserEntity_storageLabel", + "UserEntity"."externalPath" AS "UserEntity_externalPath", + "UserEntity"."oauthId" AS "UserEntity_oauthId", + "UserEntity"."profileImagePath" AS "UserEntity_profileImagePath", + "UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword", + "UserEntity"."createdAt" AS "UserEntity_createdAt", + "UserEntity"."deletedAt" AS "UserEntity_deletedAt", + "UserEntity"."updatedAt" AS "UserEntity_updatedAt", + "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled" +FROM + "users" "UserEntity" +WHERE + (("UserEntity"."isAdmin" = $1)) + AND ("UserEntity"."deletedAt" IS NULL) +LIMIT + 1 + +-- UserRepository.hasAdmin +SELECT + 1 AS "row_exists" +FROM + ( + SELECT + 1 AS dummy_column + ) "dummy_table" +WHERE + EXISTS ( + SELECT + 1 + FROM + "users" "UserEntity" + WHERE + (("UserEntity"."isAdmin" = $1)) + AND ("UserEntity"."deletedAt" IS NULL) + ) +LIMIT + 1 + +-- UserRepository.getByEmail +SELECT + "user"."id" AS "user_id", + "user"."name" AS "user_name", + "user"."avatarColor" AS "user_avatarColor", + "user"."isAdmin" AS "user_isAdmin", + "user"."email" AS "user_email", + "user"."storageLabel" AS "user_storageLabel", + "user"."externalPath" AS "user_externalPath", + "user"."oauthId" AS "user_oauthId", + "user"."profileImagePath" AS "user_profileImagePath", + "user"."shouldChangePassword" AS "user_shouldChangePassword", + "user"."createdAt" AS "user_createdAt", + "user"."deletedAt" AS "user_deletedAt", + "user"."updatedAt" AS "user_updatedAt", + "user"."memoriesEnabled" AS "user_memoriesEnabled" +FROM + "users" "user" +WHERE + ("user"."email" = $1) + AND ("user"."deletedAt" IS NULL) + +-- UserRepository.getByStorageLabel +SELECT + "UserEntity"."id" AS "UserEntity_id", + "UserEntity"."name" AS "UserEntity_name", + "UserEntity"."avatarColor" AS "UserEntity_avatarColor", + "UserEntity"."isAdmin" AS "UserEntity_isAdmin", + "UserEntity"."email" AS "UserEntity_email", + "UserEntity"."storageLabel" AS "UserEntity_storageLabel", + "UserEntity"."externalPath" AS "UserEntity_externalPath", + "UserEntity"."oauthId" AS "UserEntity_oauthId", + "UserEntity"."profileImagePath" AS "UserEntity_profileImagePath", + "UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword", + "UserEntity"."createdAt" AS "UserEntity_createdAt", + "UserEntity"."deletedAt" AS "UserEntity_deletedAt", + "UserEntity"."updatedAt" AS "UserEntity_updatedAt", + "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled" +FROM + "users" "UserEntity" +WHERE + (("UserEntity"."storageLabel" = $1)) + AND ("UserEntity"."deletedAt" IS NULL) +LIMIT + 1 + +-- UserRepository.getByOAuthId +SELECT + "UserEntity"."id" AS "UserEntity_id", + "UserEntity"."name" AS "UserEntity_name", + "UserEntity"."avatarColor" AS "UserEntity_avatarColor", + "UserEntity"."isAdmin" AS "UserEntity_isAdmin", + "UserEntity"."email" AS "UserEntity_email", + "UserEntity"."storageLabel" AS "UserEntity_storageLabel", + "UserEntity"."externalPath" AS "UserEntity_externalPath", + "UserEntity"."oauthId" AS "UserEntity_oauthId", + "UserEntity"."profileImagePath" AS "UserEntity_profileImagePath", + "UserEntity"."shouldChangePassword" AS "UserEntity_shouldChangePassword", + "UserEntity"."createdAt" AS "UserEntity_createdAt", + "UserEntity"."deletedAt" AS "UserEntity_deletedAt", + "UserEntity"."updatedAt" AS "UserEntity_updatedAt", + "UserEntity"."memoriesEnabled" AS "UserEntity_memoriesEnabled" +FROM + "users" "UserEntity" +WHERE + (("UserEntity"."oauthId" = $1)) + AND ("UserEntity"."deletedAt" IS NULL) +LIMIT + 1 + +-- UserRepository.getUserStats +SELECT + "users"."id" AS "userId", + "users"."name" AS "userName", + COUNT("assets"."id") FILTER ( + WHERE + "assets"."type" = 'IMAGE' + AND "assets"."isVisible" + ) AS "photos", + COUNT("assets"."id") FILTER ( + WHERE + "assets"."type" = 'VIDEO' + AND "assets"."isVisible" + ) AS "videos", + COALESCE(SUM("exif"."fileSizeInByte"), 0) AS "usage" +FROM + "users" "users" + LEFT JOIN "assets" "assets" ON "assets"."ownerId" = "users"."id" + AND ("assets"."deletedAt" IS NULL) + LEFT JOIN "exif" "exif" ON "exif"."assetId" = "assets"."id" +WHERE + "users"."deletedAt" IS NULL +GROUP BY + "users"."id" +ORDER BY + "users"."createdAt" ASC diff --git a/server/src/infra/sql/user.token.repository.sql b/server/src/infra/sql/user.token.repository.sql new file mode 100644 index 000000000..e1d190622 --- /dev/null +++ b/server/src/infra/sql/user.token.repository.sql @@ -0,0 +1,46 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- UserTokenRepository.getByToken +SELECT DISTINCT + "distinctAlias"."UserTokenEntity_id" AS "ids_UserTokenEntity_id" +FROM + ( + SELECT + "UserTokenEntity"."id" AS "UserTokenEntity_id", + "UserTokenEntity"."userId" AS "UserTokenEntity_userId", + "UserTokenEntity"."createdAt" AS "UserTokenEntity_createdAt", + "UserTokenEntity"."updatedAt" AS "UserTokenEntity_updatedAt", + "UserTokenEntity"."deviceType" AS "UserTokenEntity_deviceType", + "UserTokenEntity"."deviceOS" AS "UserTokenEntity_deviceOS", + "UserTokenEntity__UserTokenEntity_user"."id" AS "UserTokenEntity__UserTokenEntity_user_id", + "UserTokenEntity__UserTokenEntity_user"."name" AS "UserTokenEntity__UserTokenEntity_user_name", + "UserTokenEntity__UserTokenEntity_user"."avatarColor" AS "UserTokenEntity__UserTokenEntity_user_avatarColor", + "UserTokenEntity__UserTokenEntity_user"."isAdmin" AS "UserTokenEntity__UserTokenEntity_user_isAdmin", + "UserTokenEntity__UserTokenEntity_user"."email" AS "UserTokenEntity__UserTokenEntity_user_email", + "UserTokenEntity__UserTokenEntity_user"."storageLabel" AS "UserTokenEntity__UserTokenEntity_user_storageLabel", + "UserTokenEntity__UserTokenEntity_user"."externalPath" AS "UserTokenEntity__UserTokenEntity_user_externalPath", + "UserTokenEntity__UserTokenEntity_user"."oauthId" AS "UserTokenEntity__UserTokenEntity_user_oauthId", + "UserTokenEntity__UserTokenEntity_user"."profileImagePath" AS "UserTokenEntity__UserTokenEntity_user_profileImagePath", + "UserTokenEntity__UserTokenEntity_user"."shouldChangePassword" AS "UserTokenEntity__UserTokenEntity_user_shouldChangePassword", + "UserTokenEntity__UserTokenEntity_user"."createdAt" AS "UserTokenEntity__UserTokenEntity_user_createdAt", + "UserTokenEntity__UserTokenEntity_user"."deletedAt" AS "UserTokenEntity__UserTokenEntity_user_deletedAt", + "UserTokenEntity__UserTokenEntity_user"."updatedAt" AS "UserTokenEntity__UserTokenEntity_user_updatedAt", + "UserTokenEntity__UserTokenEntity_user"."memoriesEnabled" AS "UserTokenEntity__UserTokenEntity_user_memoriesEnabled" + FROM + "user_token" "UserTokenEntity" + LEFT JOIN "users" "UserTokenEntity__UserTokenEntity_user" ON "UserTokenEntity__UserTokenEntity_user"."id" = "UserTokenEntity"."userId" + AND ( + "UserTokenEntity__UserTokenEntity_user"."deletedAt" IS NULL + ) + WHERE + ("UserTokenEntity"."token" = $1) + ) "distinctAlias" +ORDER BY + "UserTokenEntity_id" ASC +LIMIT + 1 + +-- UserTokenRepository.delete +DELETE FROM "user_token" +WHERE + "id" = $1 diff --git a/server/src/infra/utils/database-locks.ts b/server/src/infra/utils/database-locks.ts new file mode 100644 index 000000000..756437743 --- /dev/null +++ b/server/src/infra/utils/database-locks.ts @@ -0,0 +1,3 @@ +export enum DatabaseLock { + GeodataImport = 100, +} diff --git a/server/src/main.ts b/server/src/main.ts index b86f4f789..c43d6ea46 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,5 +1,5 @@ -import { bootstrap as adminCli } from './admin-cli/main'; -import { bootstrap as immich } from './immich/main'; +import { bootstrap as admin } from './immich-admin/main'; +import { bootstrap as server } from './immich/main'; import { bootstrap as microservices } from './microservices/main'; const immichApp = process.argv[2] || process.env.IMMICH_APP; @@ -12,13 +12,13 @@ function bootstrap() { switch (immichApp) { case 'immich': process.title = 'immich_server'; - return immich(); + return server(); case 'microservices': process.title = 'immich_microservices'; return microservices(); - case 'admin-cli': + case 'immich-admin': process.title = 'immich_admin_cli'; - return adminCli(); + return admin(); default: console.log(`Invalid app name: ${immichApp}. Expected one of immich|microservices|cli`); process.exit(1); diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts index 67d995e33..3f89fa06f 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/microservices/app.service.ts @@ -84,6 +84,7 @@ export class AppService { [JobName.QUEUE_SIDECAR]: (data) => this.metadataService.handleQueueSidecar(data), [JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data), [JobName.SIDECAR_SYNC]: () => this.metadataService.handleSidecarSync(), + [JobName.SIDECAR_WRITE]: (data) => this.metadataService.handleSidecarWrite(data), [JobName.LIBRARY_SCAN_ASSET]: (data) => this.libraryService.handleAssetRefresh(data), [JobName.LIBRARY_SCAN]: (data) => this.libraryService.handleQueueAssetRefresh(data), [JobName.LIBRARY_DELETE]: (data) => this.libraryService.handleDeleteLibrary(data), @@ -92,16 +93,6 @@ export class AppService { [JobName.LIBRARY_QUEUE_CLEANUP]: () => this.libraryService.handleQueueCleanup(), }); - process.on('uncaughtException', async (error: Error | any) => { - const isCsvError = error.code === 'CSV_RECORD_INCONSISTENT_FIELDS_LENGTH'; - if (!isCsvError) { - throw error; - } - - this.logger.warn('Geocoding csv parse error, trying again without cache...'); - await this.metadataService.init(true); - }); - await this.metadataService.init(); await this.searchService.init(); } diff --git a/server/start.sh b/server/start.sh index 5ec1a2643..7aa0bc20d 100755 --- a/server/start.sh +++ b/server/start.sh @@ -32,4 +32,4 @@ if [ "$REDIS_PASSWORD_FILE" ]; then unset REDIS_PASSWORD_FILE fi -exec node dist/main $@ +exec node /usr/src/app/dist/main $@ diff --git a/server/test/api/asset-api.ts b/server/test/api/asset-api.ts index 0c83b8abb..e17e63a22 100644 --- a/server/test/api/asset-api.ts +++ b/server/test/api/asset-api.ts @@ -6,7 +6,33 @@ import request from 'supertest'; type UploadDto = Partial & { content?: Buffer }; +const asset = { + deviceAssetId: 'test-1', + deviceId: 'test', + fileCreatedAt: new Date(), + fileModifiedAt: new Date(), +}; + export const assetApi = { + create: async ( + server: any, + accessToken: string, + dto?: Omit, + ): Promise => { + dto = dto || asset; + const { status, body } = await request(server) + .post(`/asset/upload`) + .field('deviceAssetId', dto.deviceAssetId) + .field('deviceId', dto.deviceId) + .field('fileCreatedAt', dto.fileCreatedAt.toISOString()) + .field('fileModifiedAt', dto.fileModifiedAt.toISOString()) + .attach('assetData', randomBytes(32), 'example.jpg') + .set('Authorization', `Bearer ${accessToken}`); + + expect([200, 201].includes(status)).toBe(true); + + return body as AssetResponseDto; + }, get: async (server: any, accessToken: string, id: string): Promise => { const { body, status } = await request(server) .get(`/asset/assetById/${id}`) diff --git a/server/test/api/auth-api.ts b/server/test/api/auth-api.ts index 5f1750963..3043c941f 100644 --- a/server/test/api/auth-api.ts +++ b/server/test/api/auth-api.ts @@ -1,5 +1,5 @@ -import { AdminSignupResponseDto, AuthDeviceResponseDto, LoginCredentialDto, LoginResponseDto } from '@app/domain'; -import { adminSignupStub, loginResponseStub, loginStub, signupResponseStub } from '@test'; +import { AuthDeviceResponseDto, LoginCredentialDto, LoginResponseDto, UserResponseDto } from '@app/domain'; +import { adminSignupStub, loginResponseStub, loginStub } from '@test'; import request from 'supertest'; export const authApi = { @@ -7,9 +7,8 @@ export const authApi = { const { status, body } = await request(server).post('/auth/admin-sign-up').send(adminSignupStub); expect(status).toBe(201); - expect(body).toEqual(signupResponseStub); - return body as AdminSignupResponseDto; + return body as UserResponseDto; }, adminLogin: async (server: any) => { const { status, body } = await request(server).post('/auth/login').send(loginStub.admin); diff --git a/server/test/api/index.ts b/server/test/api/index.ts index d9321df27..55cf2526d 100644 --- a/server/test/api/index.ts +++ b/server/test/api/index.ts @@ -3,6 +3,7 @@ import { albumApi } from './album-api'; import { assetApi } from './asset-api'; import { authApi } from './auth-api'; import { libraryApi } from './library-api'; +import { partnerApi } from './partner-api'; import { sharedLinkApi } from './shared-link-api'; import { userApi } from './user-api'; @@ -14,4 +15,5 @@ export const api = { sharedLinkApi, albumApi, userApi, + partnerApi, }; diff --git a/server/test/api/partner-api.ts b/server/test/api/partner-api.ts new file mode 100644 index 000000000..97a9558c5 --- /dev/null +++ b/server/test/api/partner-api.ts @@ -0,0 +1,10 @@ +import { PartnerResponseDto } from '@app/domain'; +import request from 'supertest'; + +export const partnerApi = { + create: async (server: any, accessToken: string, id: string) => { + const { status, body } = await request(server).post(`/partner/${id}`).set('Authorization', `Bearer ${accessToken}`); + expect(status).toBe(201); + return body as PartnerResponseDto; + }, +}; diff --git a/server/test/db/index.ts b/server/test/db/index.ts deleted file mode 100644 index dd92f0e27..000000000 --- a/server/test/db/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { dataSource } from '@app/infra'; - -export const db = { - reset: async () => { - if (!dataSource.isInitialized) { - await dataSource.initialize(); - } - - await dataSource.transaction(async (em) => { - for (const entity of dataSource.entityMetadatas) { - if (entity.tableName === 'users') { - continue; - } - await em.query(`DELETE FROM ${entity.tableName} CASCADE;`); - } - await em.query(`DELETE FROM "users" CASCADE;`); - }); - }, - disconnect: async () => { - if (dataSource.isInitialized) { - await dataSource.destroy(); - } - }, -}; diff --git a/server/test/e2e/activity.e2e-spec.ts b/server/test/e2e/activity.e2e-spec.ts index 5cc86fc6a..3fe38e715 100644 --- a/server/test/e2e/activity.e2e-spec.ts +++ b/server/test/e2e/activity.e2e-spec.ts @@ -1,9 +1,9 @@ import { AlbumResponseDto, LoginResponseDto, ReactionType } from '@app/domain'; import { ActivityController } from '@app/immich'; import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; +import { ActivityEntity } from '@app/infra/entities'; import { api } from '@test/api'; -import { db } from '@test/db'; -import { errorStub, uuidStub } from '@test/fixtures'; +import { errorStub, userDto, uuidStub } from '@test/fixtures'; import { testApp } from '@test/test-utils'; import request from 'supertest'; @@ -12,9 +12,23 @@ describe(`${ActivityController.name} (e2e)`, () => { let admin: LoginResponseDto; let asset: AssetFileUploadResponseDto; let album: AlbumResponseDto; + let nonOwner: LoginResponseDto; beforeAll(async () => { [server] = await testApp.create(); + await testApp.reset(); + await api.authApi.adminSignUp(server); + admin = await api.authApi.adminLogin(server); + asset = await api.assetApi.upload(server, admin.accessToken, 'example'); + + await api.userApi.create(server, admin.accessToken, userDto.user1); + nonOwner = await api.authApi.login(server, userDto.user1); + + album = await api.albumApi.create(server, admin.accessToken, { + albumName: 'Album 1', + assetIds: [asset.id], + sharedWithUserIds: [nonOwner.userId], + }); }); afterAll(async () => { @@ -22,11 +36,7 @@ describe(`${ActivityController.name} (e2e)`, () => { }); beforeEach(async () => { - await db.reset(); - await api.authApi.adminSignUp(server); - admin = await api.authApi.adminLogin(server); - asset = await api.assetApi.upload(server, admin.accessToken, 'example'); - album = await api.albumApi.create(server, admin.accessToken, { albumName: 'Album 1', assetIds: [asset.id] }); + await testApp.reset({ entities: [ActivityEntity] }); }); describe('GET /activity', () => { @@ -247,6 +257,20 @@ describe(`${ActivityController.name} (e2e)`, () => { expect(body).toEqual(reaction); }); + it('should not confuse an album like with an asset like', async () => { + const reaction = await api.activityApi.create(server, admin.accessToken, { + albumId: album.id, + assetId: asset.id, + type: ReactionType.LIKE, + }); + const { status, body } = await request(server) + .post('/activity') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ albumId: album.id, type: 'like' }); + expect(status).toEqual(201); + expect(body.id).not.toEqual(reaction.id); + }); + it('should add a comment to an asset', async () => { const { status, body } = await request(server) .post('/activity') @@ -333,14 +357,6 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should let the owner remove a comment by another user', async () => { - const { id: userId } = await api.userApi.create(server, admin.accessToken, { - email: 'user1@immich.app', - password: 'Password123', - firstName: 'User 1', - lastName: 'Test', - }); - await api.albumApi.addUsers(server, admin.accessToken, album.id, { sharedUserIds: [userId] }); - const nonOwner = await api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' }); const reaction = await api.activityApi.create(server, nonOwner.accessToken, { albumId: album.id, type: ReactionType.COMMENT, @@ -350,18 +366,11 @@ describe(`${ActivityController.name} (e2e)`, () => { const { status } = await request(server) .delete(`/activity/${reaction.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toEqual(204); }); it('should not let a user remove a comment by another user', async () => { - const { id: userId } = await api.userApi.create(server, admin.accessToken, { - email: 'user1@immich.app', - password: 'Password123', - firstName: 'User 1', - lastName: 'Test', - }); - await api.albumApi.addUsers(server, admin.accessToken, album.id, { sharedUserIds: [userId] }); - const nonOwner = await api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' }); const reaction = await api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.COMMENT, @@ -371,19 +380,12 @@ describe(`${ActivityController.name} (e2e)`, () => { const { status, body } = await request(server) .delete(`/activity/${reaction.id}`) .set('Authorization', `Bearer ${nonOwner.accessToken}`); + expect(status).toBe(400); expect(body).toEqual(errorStub.badRequest('Not found or no activity.delete access')); }); it('should let a non-owner remove their own comment', async () => { - const { id: userId } = await api.userApi.create(server, admin.accessToken, { - email: 'user1@immich.app', - password: 'Password123', - firstName: 'User 1', - lastName: 'Test', - }); - await api.albumApi.addUsers(server, admin.accessToken, album.id, { sharedUserIds: [userId] }); - const nonOwner = await api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' }); const reaction = await api.activityApi.create(server, nonOwner.accessToken, { albumId: album.id, type: ReactionType.COMMENT, @@ -393,6 +395,7 @@ describe(`${ActivityController.name} (e2e)`, () => { const { status } = await request(server) .delete(`/activity/${reaction.id}`) .set('Authorization', `Bearer ${nonOwner.accessToken}`); + expect(status).toBe(204); }); }); diff --git a/server/test/e2e/album.e2e-spec.ts b/server/test/e2e/album.e2e-spec.ts index e10f5414f..8058d9593 100644 --- a/server/test/e2e/album.e2e-spec.ts +++ b/server/test/e2e/album.e2e-spec.ts @@ -3,8 +3,7 @@ import { AlbumController } from '@app/immich'; import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; import { SharedLinkType } from '@app/infra/entities'; import { api } from '@test/api'; -import { db } from '@test/db'; -import { errorStub, uuidStub } from '@test/fixtures'; +import { errorStub, userDto, uuidStub } from '@test/fixtures'; import { testApp } from '@test/test-utils'; import request from 'supertest'; @@ -33,28 +32,18 @@ describe(`${AlbumController.name} (e2e)`, () => { }); beforeEach(async () => { - await db.reset(); + await testApp.reset(); await api.authApi.adminSignUp(server); admin = await api.authApi.adminLogin(server); await Promise.all([ - api.userApi.create(server, admin.accessToken, { - email: 'user1@immich.app', - password: 'Password123', - firstName: 'User 1', - lastName: 'Test', - }), - api.userApi.create(server, admin.accessToken, { - email: 'user2@immich.app', - password: 'Password123', - firstName: 'User 2', - lastName: 'Test', - }), + api.userApi.create(server, admin.accessToken, userDto.user1), + api.userApi.create(server, admin.accessToken, userDto.user2), ]); [user1, user2] = await Promise.all([ - api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' }), - api.authApi.login(server, { email: 'user2@immich.app', password: 'Password123' }), + api.authApi.login(server, userDto.user1), + api.authApi.login(server, userDto.user2), ]); user1Asset = await api.assetApi.upload(server, user1.accessToken, 'example'); @@ -226,6 +215,7 @@ describe(`${AlbumController.name} (e2e)`, () => { assets: [], assetCount: 0, owner: expect.objectContaining({ email: user1.userEmail }), + isActivityEnabled: true, }); }); }); @@ -256,7 +246,7 @@ describe(`${AlbumController.name} (e2e)`, () => { it('should return album info for own album', async () => { const { status, body } = await request(server) - .get(`/album/${user1Albums[0].id}`) + .get(`/album/${user1Albums[0].id}?withoutAssets=false`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); @@ -265,12 +255,34 @@ describe(`${AlbumController.name} (e2e)`, () => { it('should return album info for shared album', async () => { const { status, body } = await request(server) - .get(`/album/${user2Albums[0].id}`) + .get(`/album/${user2Albums[0].id}?withoutAssets=false`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toEqual(user2Albums[0]); }); + + it('should return album info with assets when withoutAssets is undefined', async () => { + const { status, body } = await request(server) + .get(`/album/${user1Albums[0].id}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual(user1Albums[0]); + }); + + it('should return album info without assets when withoutAssets is true', async () => { + const { status, body } = await request(server) + .get(`/album/${user1Albums[0].id}?withoutAssets=true`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ + ...user1Albums[0], + assets: [], + assetCount: 1, + }); + }); }); describe('PUT /album/:id/assets', () => { diff --git a/server/test/e2e/asset.e2e-spec.ts b/server/test/e2e/asset.e2e-spec.ts index a6f8f48e7..95cc92b6a 100644 --- a/server/test/e2e/asset.e2e-spec.ts +++ b/server/test/e2e/asset.e2e-spec.ts @@ -4,31 +4,24 @@ import { IPersonRepository, LibraryResponseDto, LoginResponseDto, - SharedLinkResponseDto, TimeBucketSize, + WithoutProperty, + mapAsset, + usePagination, } from '@app/domain'; import { AssetController } from '@app/immich'; -import { AssetEntity, AssetType, SharedLinkType } from '@app/infra/entities'; +import { AssetEntity, AssetType, LibraryType, SharedLinkType } from '@app/infra/entities'; +import { AssetRepository } from '@app/infra/repositories'; import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; -import { errorStub, uuidStub } from '@test/fixtures'; -import { db, testApp } from '@test/test-utils'; +import { errorStub, userDto, uuidStub } from '@test/fixtures'; +import { testApp } from '@test/test-utils'; import { randomBytes } from 'crypto'; +import { DateTime } from 'luxon'; import request from 'supertest'; -const user1Dto = { - email: 'user1@immich.app', - password: 'Password123', - firstName: 'User 1', - lastName: 'Test', -}; - -const user2Dto = { - email: 'user2@immich.app', - password: 'Password123', - firstName: 'User 2', - lastName: 'Test', -}; +const today = DateTime.fromObject({ year: 2023, month: 11, day: 3 }); +const yesterday = today.minus({ days: 1 }); const makeUploadDto = (options?: { omit: string }): Record => { const dto: Record = { @@ -48,81 +41,521 @@ const makeUploadDto = (options?: { omit: string }): Record => { return dto; }; -let assetCount = 0; -const createAsset = ( - repository: IAssetRepository, - loginResponse: LoginResponseDto, - libraryId: string, - createdAt: Date, -): Promise => { - const id = assetCount++; - return repository.create({ - ownerId: loginResponse.userId, - checksum: randomBytes(20), - originalPath: `/tests/test_${id}`, - deviceAssetId: `test_${id}`, - deviceId: 'e2e-test', - libraryId, - isVisible: true, - fileCreatedAt: createdAt, - fileModifiedAt: new Date(), - localDateTime: createdAt, - type: AssetType.IMAGE, - originalFileName: `test_${id}`, - }); -}; - describe(`${AssetController.name} (e2e)`, () => { let app: INestApplication; let server: any; let assetRepository: IAssetRepository; - let defaultLibrary: LibraryResponseDto; - let sharedLink: SharedLinkResponseDto; let user1: LoginResponseDto; let user2: LoginResponseDto; - let asset1: AssetEntity; - let asset2: AssetEntity; - let asset3: AssetEntity; - let asset4: AssetEntity; + let libraries: LibraryResponseDto[]; + let asset1: AssetResponseDto; + let asset2: AssetResponseDto; + let asset3: AssetResponseDto; + let asset4: AssetResponseDto; + let asset5: AssetResponseDto; + + let assetCount = 0; + const createAsset = async (loginResponse: LoginResponseDto, createdAt: Date, other: Partial = {}) => { + const id = assetCount++; + const asset = await assetRepository.create({ + createdAt: today.toJSDate(), + updatedAt: today.toJSDate(), + ownerId: loginResponse.userId, + checksum: randomBytes(20), + originalPath: `/tests/test_${id}`, + deviceAssetId: `test_${id}`, + deviceId: 'e2e-test', + libraryId: ( + libraries.find( + ({ ownerId, type }) => ownerId === loginResponse.userId && type === LibraryType.UPLOAD, + ) as LibraryResponseDto + ).id, + isVisible: true, + fileCreatedAt: createdAt, + fileModifiedAt: new Date(), + localDateTime: createdAt, + type: AssetType.IMAGE, + originalFileName: `test_${id}`, + ...other, + }); + + return mapAsset(asset); + }; beforeAll(async () => { [server, app] = await testApp.create(); assetRepository = app.get(IAssetRepository); + + await testApp.reset(); + + await api.authApi.adminSignUp(server); + const admin = await api.authApi.adminLogin(server); + + await Promise.all([ + api.userApi.create(server, admin.accessToken, userDto.user1), + api.userApi.create(server, admin.accessToken, userDto.user2), + ]); + + [user1, user2] = await Promise.all([ + api.authApi.login(server, userDto.user1), + api.authApi.login(server, userDto.user2), + ]); + + const [user1Libraries, user2Libraries] = await Promise.all([ + api.libraryApi.getAll(server, user1.accessToken), + api.libraryApi.getAll(server, user2.accessToken), + ]); + + libraries = [...user1Libraries, ...user2Libraries]; + }); + + beforeEach(async () => { + await testApp.reset({ entities: [AssetEntity] }); + + [asset1, asset2, asset3, asset4, asset5] = await Promise.all([ + createAsset(user1, new Date('1970-01-01')), + createAsset(user1, new Date('1970-02-10')), + createAsset(user1, new Date('1970-02-11'), { + isFavorite: true, + isArchived: true, + isExternal: true, + isReadOnly: true, + type: AssetType.VIDEO, + fileCreatedAt: yesterday.toJSDate(), + fileModifiedAt: yesterday.toJSDate(), + createdAt: yesterday.toJSDate(), + updatedAt: yesterday.toJSDate(), + localDateTime: yesterday.toJSDate(), + }), + createAsset(user2, new Date('1970-01-01')), + createAsset(user1, new Date('1970-01-01'), { + deletedAt: yesterday.toJSDate(), + }), + ]); + }); + + beforeEach(async () => { + await testApp.reset({ entities: [AssetEntity] }); + + [asset1, asset2, asset3, asset4, asset5] = await Promise.all([ + createAsset(user1, new Date('1970-01-01')), + createAsset(user1, new Date('1970-02-10')), + createAsset(user1, new Date('1970-02-11'), { + isFavorite: true, + isArchived: true, + isExternal: true, + isReadOnly: true, + type: AssetType.VIDEO, + fileCreatedAt: yesterday.toJSDate(), + fileModifiedAt: yesterday.toJSDate(), + createdAt: yesterday.toJSDate(), + updatedAt: yesterday.toJSDate(), + localDateTime: yesterday.toJSDate(), + encodedVideoPath: '/path/to/encoded-video.mp4', + webpPath: '/path/to/thumb.webp', + resizePath: '/path/to/thumb.jpg', + }), + createAsset(user2, new Date('1970-01-01')), + createAsset(user1, new Date('1970-01-01'), { + deletedAt: yesterday.toJSDate(), + }), + ]); + + await assetRepository.upsertExif({ + assetId: asset3.id, + latitude: 90, + longitude: 90, + city: 'Immich', + state: 'Nebraska', + country: 'United States', + make: 'Cannon', + model: 'EOS Rebel T7', + lensModel: 'Fancy lens', + }); }); afterAll(async () => { await testApp.teardown(); }); - beforeEach(async () => { - await db.reset(); - await api.authApi.adminSignUp(server); - const admin = await api.authApi.adminLogin(server); - - const [libraries] = await Promise.all([ - api.libraryApi.getAll(server, admin.accessToken), - api.userApi.create(server, admin.accessToken, user1Dto), - api.userApi.create(server, admin.accessToken, user2Dto), - ]); - - defaultLibrary = libraries[0]; - - [user1, user2] = await Promise.all([ - api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }), - api.authApi.login(server, { email: user2Dto.email, password: user2Dto.password }), - ]); - - [asset1, asset2, asset3, asset4] = await Promise.all([ - createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-01-01')), - createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-01-02')), - createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')), - createAsset(assetRepository, user2, defaultLibrary.id, new Date('1970-01-01')), - ]); - - sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.INDIVIDUAL, - assetIds: [asset1.id, asset2.id], + describe('GET /assets', () => { + it('should require authentication', async () => { + const { status, body } = await request(server).get('/assets'); + expect(body).toEqual(errorStub.unauthorized); + expect(status).toBe(401); }); + + const badTests = [ + // + { + should: 'should reject page as a string', + query: { page: 'abc' }, + expected: ['page must not be less than 1', 'page must be an integer number'], + }, + { + should: 'should reject page as a decimal', + query: { page: 1.5 }, + expected: ['page must be an integer number'], + }, + { + should: 'should reject page as a negative number', + query: { page: -10 }, + expected: ['page must not be less than 1'], + }, + { + should: 'should reject page as 0', + query: { page: 0 }, + expected: ['page must not be less than 1'], + }, + { + should: 'should reject size as a string', + query: { size: 'abc' }, + expected: ['size must not be less than 1', 'size must be an integer number'], + }, + { + should: 'should reject an invalid size', + query: { size: -1.5 }, + expected: ['size must not be less than 1', 'size must be an integer number'], + }, + ...[ + 'isArchived', + 'isFavorite', + 'isReadOnly', + 'isExternal', + 'isEncoded', + 'isMotion', + 'isOffline', + 'isVisible', + ].map((value) => ({ + should: `should reject ${value} not a boolean`, + query: { [value]: 'immich' }, + expected: [`${value} must be a boolean value`], + })), + ]; + + for (const { should, query, expected } of badTests) { + it(should, async () => { + const { status, body } = await request(server) + .get('/assets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .query(query); + expect(status).toBe(400); + expect(body).toEqual(errorStub.badRequest(expected)); + }); + } + + const searchTests = [ + { + should: 'should only return my own assets', + deferred: () => ({ + query: {}, + assets: [asset3, asset2, asset1], + }), + }, + { + should: 'should sort my assets in reverse', + deferred: () => ({ + query: { order: 'asc' }, + assets: [asset1, asset2, asset3], + }), + }, + { + should: 'should support custom page sizes', + deferred: () => ({ + query: { size: 1 }, + assets: [asset3], + }), + }, + { + should: 'should support pagination', + deferred: () => ({ + query: { size: 1, page: 2 }, + assets: [asset2], + }), + }, + { + should: 'should search by checksum (base64)', + deferred: () => ({ + query: { checksum: asset1.checksum }, + assets: [asset1], + }), + }, + { + should: 'should search by checksum (hex)', + deferred: () => ({ + query: { checksum: Buffer.from(asset1.checksum, 'base64').toString('hex') }, + assets: [asset1], + }), + }, + { + should: 'should search by id', + deferred: () => ({ + query: { id: asset1.id }, + assets: [asset1], + }), + }, + { + should: 'should search by isFavorite (true)', + deferred: () => ({ + query: { isFavorite: true }, + assets: [asset3], + }), + }, + { + should: 'should search by isFavorite (false)', + deferred: () => ({ + query: { isFavorite: false }, + assets: [asset2, asset1], + }), + }, + { + should: 'should search by isArchived (true)', + deferred: () => ({ + query: { isArchived: true }, + assets: [asset3], + }), + }, + { + should: 'should search by isArchived (false)', + deferred: () => ({ + query: { isArchived: false }, + assets: [asset2, asset1], + }), + }, + { + should: 'should search by isReadOnly (true)', + deferred: () => ({ + query: { isReadOnly: true }, + assets: [asset3], + }), + }, + { + should: 'should search by isReadOnly (false)', + deferred: () => ({ + query: { isReadOnly: false }, + assets: [asset2, asset1], + }), + }, + { + should: 'should search by type (image)', + deferred: () => ({ + query: { type: 'IMAGE' }, + assets: [asset2, asset1], + }), + }, + { + should: 'should search by type (video)', + deferred: () => ({ + query: { type: 'VIDEO' }, + assets: [asset3], + }), + }, + { + should: 'should search by createdBefore', + deferred: () => ({ + query: { createdBefore: yesterday.plus({ hour: 1 }).toJSDate() }, + assets: [asset3], + }), + }, + { + should: 'should search by createdBefore (no results)', + deferred: () => ({ + query: { createdBefore: yesterday.minus({ hour: 1 }).toJSDate() }, + assets: [], + }), + }, + { + should: 'should search by createdAfter', + deferred: () => ({ + query: { createdAfter: yesterday.minus({ hour: 1 }).toJSDate() }, + assets: [asset3, asset2, asset1], + }), + }, + { + should: 'should search by createdAfter (no results)', + deferred: () => ({ + query: { createdAfter: today.plus({ hour: 1 }).toJSDate() }, + assets: [], + }), + }, + { + should: 'should search by updatedBefore', + deferred: () => ({ + query: { updatedBefore: yesterday.plus({ hour: 1 }).toJSDate() }, + assets: [asset3], + }), + }, + { + should: 'should search by updatedBefore (no results)', + deferred: () => ({ + query: { updatedBefore: yesterday.minus({ hour: 1 }).toJSDate() }, + assets: [], + }), + }, + { + should: 'should search by updatedAfter', + deferred: () => ({ + query: { updatedAfter: yesterday.minus({ hour: 1 }).toJSDate() }, + assets: [asset3, asset2, asset1], + }), + }, + { + should: 'should search by updatedAfter (no results)', + deferred: () => ({ + query: { updatedAfter: today.plus({ hour: 1 }).toJSDate() }, + assets: [], + }), + }, + { + should: 'should search by trashedBefore', + deferred: () => ({ + query: { trashedBefore: yesterday.plus({ hour: 1 }).toJSDate() }, + assets: [asset5], + }), + }, + { + should: 'should search by trashedBefore (no results)', + deferred: () => ({ + query: { trashedBefore: yesterday.minus({ hour: 1 }).toJSDate() }, + assets: [], + }), + }, + { + should: 'should search by trashedAfter', + deferred: () => ({ + query: { trashedAfter: yesterday.minus({ hour: 1 }).toJSDate() }, + assets: [asset5], + }), + }, + { + should: 'should search by trashedAfter (no results)', + deferred: () => ({ + query: { trashedAfter: today.plus({ hour: 1 }).toJSDate() }, + assets: [], + }), + }, + { + should: 'should search by takenBefore', + deferred: () => ({ + query: { takenBefore: yesterday.plus({ hour: 1 }).toJSDate() }, + assets: [asset3, asset2, asset1], + }), + }, + { + should: 'should search by takenBefore (no results)', + deferred: () => ({ + query: { takenBefore: yesterday.minus({ years: 100 }).toJSDate() }, + assets: [], + }), + }, + { + should: 'should search by takenAfter', + deferred: () => ({ + query: { takenAfter: yesterday.minus({ hour: 1 }).toJSDate() }, + assets: [asset3], + }), + }, + { + should: 'should search by takenAfter (no results)', + deferred: () => ({ + query: { takenAfter: today.plus({ hour: 1 }).toJSDate() }, + assets: [], + }), + }, + { + should: 'should search by originalPath', + deferred: () => ({ + query: { originalPath: asset1.originalPath }, + assets: [asset1], + }), + }, + { + should: 'should search by originalFilename', + deferred: () => ({ + query: { originalFileName: asset1.originalFileName }, + assets: [asset1], + }), + }, + { + should: 'should search by encodedVideoPath', + deferred: () => ({ + query: { encodedVideoPath: '/path/to/encoded-video.mp4' }, + assets: [asset3], + }), + }, + { + should: 'should search by resizePath', + deferred: () => ({ + query: { resizePath: '/path/to/thumb.jpg' }, + assets: [asset3], + }), + }, + { + should: 'should search by webpPath', + deferred: () => ({ + query: { webpPath: '/path/to/thumb.webp' }, + assets: [asset3], + }), + }, + { + should: 'should search by city', + deferred: () => ({ + query: { city: 'Immich' }, + assets: [asset3], + }), + }, + { + should: 'should search by state', + deferred: () => ({ + query: { state: 'Nebraska' }, + assets: [asset3], + }), + }, + { + should: 'should search by country', + deferred: () => ({ + query: { country: 'United States' }, + assets: [asset3], + }), + }, + { + should: 'sohuld search by make', + deferred: () => ({ + query: { make: 'Cannon' }, + assets: [asset3], + }), + }, + { + should: 'should search by country', + deferred: () => ({ + query: { model: 'EOS Rebel T7' }, + assets: [asset3], + }), + }, + { + should: 'should search by lensModel', + deferred: () => ({ + query: { lensModel: 'Fancy lens' }, + assets: [asset3], + }), + }, + ]; + + for (const { should, deferred } of searchTests) { + it(should, async () => { + const { assets, query } = deferred(); + const { status, body } = await request(server) + .get('/assets') + .query(query) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body.length).toBe(assets.length); + for (let i = 0; i < assets.length; i++) { + expect(body[i]).toEqual(expect.objectContaining({ id: assets[i].id })); + } + }); + } }); describe('POST /asset/upload', () => { @@ -267,6 +700,54 @@ describe(`${AssetController.name} (e2e)`, () => { expect(status).toEqual(200); }); + it('should update date time original', async () => { + const { status, body } = await request(server) + .put(`/asset/${asset1.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' }); + + expect(body).toMatchObject({ + id: asset1.id, + exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00.000Z' }), + }); + expect(status).toEqual(200); + }); + + it('should reject invalid gps coordinates', async () => { + for (const test of [ + { latitude: 12 }, + { longitude: 12 }, + { latitude: 12, longitude: 'abc' }, + { latitude: 'abc', longitude: 12 }, + { latitude: null, longitude: 12 }, + { latitude: 12, longitude: null }, + { latitude: 91, longitude: 12 }, + { latitude: -91, longitude: 12 }, + { latitude: 12, longitude: -181 }, + { latitude: 12, longitude: 181 }, + ]) { + const { status, body } = await request(server) + .put(`/asset/${asset1.id}`) + .send(test) + .set('Authorization', `Bearer ${user1.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorStub.badRequest()); + } + }); + + it('should update gps data', async () => { + const { status, body } = await request(server) + .put(`/asset/${asset1.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ latitude: 12, longitude: 12 }); + + expect(body).toMatchObject({ + id: asset1.id, + exifInfo: expect.objectContaining({ latitude: 12, longitude: 12 }), + }); + expect(status).toEqual(200); + }); + it('should set the description', async () => { const { status, body } = await request(server) .put(`/asset/${asset1.id}`) @@ -368,8 +849,8 @@ describe(`${AssetController.name} (e2e)`, () => { .get('/asset/statistics') .set('Authorization', `Bearer ${user1.accessToken}`); + expect(body).toEqual({ images: 5, videos: 1, total: 6 }); expect(status).toBe(200); - expect(body).toEqual({ images: 6, videos: 0, total: 6 }); }); it('should return stats of all favored assets', async () => { @@ -379,7 +860,7 @@ describe(`${AssetController.name} (e2e)`, () => { .query({ isFavorite: true }); expect(status).toBe(200); - expect(body).toEqual({ images: 2, videos: 0, total: 2 }); + expect(body).toEqual({ images: 2, videos: 1, total: 3 }); }); it('should return stats of all archived assets', async () => { @@ -389,7 +870,7 @@ describe(`${AssetController.name} (e2e)`, () => { .query({ isArchived: true }); expect(status).toBe(200); - expect(body).toEqual({ images: 2, videos: 0, total: 2 }); + expect(body).toEqual({ images: 2, videos: 1, total: 3 }); }); it('should return stats of all favored and archived assets', async () => { @@ -399,7 +880,7 @@ describe(`${AssetController.name} (e2e)`, () => { .query({ isFavorite: true, isArchived: true }); expect(status).toBe(200); - expect(body).toEqual({ images: 1, videos: 0, total: 1 }); + expect(body).toEqual({ images: 1, videos: 1, total: 2 }); }); it('should return stats of all assets neither favored nor archived', async () => { @@ -409,19 +890,19 @@ describe(`${AssetController.name} (e2e)`, () => { .query({ isFavorite: false, isArchived: false }); expect(status).toBe(200); - expect(body).toEqual({ images: 3, videos: 0, total: 3 }); + expect(body).toEqual({ images: 2, videos: 0, total: 2 }); }); }); describe('GET /asset/random', () => { beforeAll(async () => { await Promise.all([ - createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')), - createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')), - createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')), - createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')), - createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')), - createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-02-01')), + createAsset(user1, new Date('1970-02-01')), + createAsset(user1, new Date('1970-02-01')), + createAsset(user1, new Date('1970-02-01')), + createAsset(user1, new Date('1970-02-01')), + createAsset(user1, new Date('1970-02-01')), + createAsset(user1, new Date('1970-02-01')), ]); }); it('should require authentication', async () => { @@ -431,7 +912,7 @@ describe(`${AssetController.name} (e2e)`, () => { expect(body).toEqual(errorStub.unauthorized); }); - it('should return 1 random assets', async () => { + it.each(Array(10))('should return 1 random assets', async () => { const { status, body } = await request(server) .get('/asset/random') .set('Authorization', `Bearer ${user1.accessToken}`); @@ -441,13 +922,14 @@ describe(`${AssetController.name} (e2e)`, () => { const assets: AssetResponseDto[] = body; expect(assets.length).toBe(1); expect(assets[0].ownerId).toBe(user1.userId); - // assets owned by user1 - expect([asset1.id, asset2.id, asset3.id]).toContain(assets[0].id); + // // assets owned by user2 expect(assets[0].id).not.toBe(asset4.id); + // assets owned by user1 + expect([asset1.id, asset2.id, asset3.id]).toContain(assets[0].id); }); - it('should return 2 random assets', async () => { + it.each(Array(10))('should return 2 random assets', async () => { const { status, body } = await request(server) .get('/asset/random?count=2') .set('Authorization', `Bearer ${user1.accessToken}`); @@ -504,13 +986,19 @@ describe(`${AssetController.name} (e2e)`, () => { expect(status).toBe(200); expect(body).toEqual( expect.arrayContaining([ - { count: 1, timeBucket: asset3.fileCreatedAt.toISOString() }, - { count: 2, timeBucket: asset1.fileCreatedAt.toISOString() }, + { count: 1, timeBucket: '2023-11-01T00:00:00.000Z' }, + { count: 1, timeBucket: '1970-01-01T00:00:00.000Z' }, + { count: 1, timeBucket: '1970-02-01T00:00:00.000Z' }, ]), ); }); it('should not allow access for unrelated shared links', async () => { + const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, { + type: SharedLinkType.INDIVIDUAL, + assetIds: [asset1.id, asset2.id], + }); + const { status, body } = await request(server) .get('/asset/time-buckets') .query({ key: sharedLink.key, size: TimeBucketSize.MONTH }); @@ -574,12 +1062,53 @@ describe(`${AssetController.name} (e2e)`, () => { .query({ size: TimeBucketSize.MONTH, timeBucket }); expect(status).toBe(200); - expect(body).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: asset1.id }), - expect.objectContaining({ id: asset2.id }), - ]), - ); + expect(body).toEqual(expect.arrayContaining([expect.objectContaining({ id: asset2.id })])); + }); + + it('should return error if time bucket is requested with partners asset and archived', async () => { + const req1 = await request(server) + .get('/asset/time-buckets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .query({ size: TimeBucketSize.MONTH, withPartners: true, isArchived: true }); + + expect(req1.status).toBe(400); + expect(req1.body).toEqual(errorStub.badRequest()); + + const req2 = await request(server) + .get('/asset/time-buckets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .query({ size: TimeBucketSize.MONTH, withPartners: true, isArchived: undefined }); + + expect(req2.status).toBe(400); + expect(req2.body).toEqual(errorStub.badRequest()); + }); + + it('should return error if time bucket is requested with partners asset and favorite', async () => { + const req1 = await request(server) + .get('/asset/time-buckets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .query({ size: TimeBucketSize.MONTH, withPartners: true, isFavorite: true }); + + expect(req1.status).toBe(400); + expect(req1.body).toEqual(errorStub.badRequest()); + + const req2 = await request(server) + .get('/asset/time-buckets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .query({ size: TimeBucketSize.MONTH, withPartners: true, isFavorite: false }); + + expect(req2.status).toBe(400); + expect(req2.body).toEqual(errorStub.badRequest()); + }); + + it('should return error if time bucket is requested with partners asset and trash', async () => { + const req = await request(server) + .get('/asset/time-buckets') + .set('Authorization', `Bearer ${user1.accessToken}`) + .query({ size: TimeBucketSize.MONTH, withPartners: true, isTrashed: true }); + + expect(req.status).toBe(400); + expect(req.body).toEqual(errorStub.badRequest()); }); }); @@ -602,16 +1131,12 @@ describe(`${AssetController.name} (e2e)`, () => { it('should get map markers for all non-archived assets', async () => { const { status, body } = await request(server) .get('/asset/map-marker') + .query({ isArchived: false }) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); - expect(body).toHaveLength(2); - expect(body).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: asset1.id }), - expect.objectContaining({ id: asset2.id }), - ]), - ); + expect(body).toHaveLength(1); + expect(body).toEqual(expect.arrayContaining([expect.objectContaining({ id: asset2.id })])); }); it('should get all map markers', async () => { @@ -664,8 +1189,10 @@ describe(`${AssetController.name} (e2e)`, () => { }); it('should add stack children', async () => { - const parent = await createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-01-01')); - const child = await createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-01-01')); + const [parent, child] = await Promise.all([ + createAsset(user1, new Date('1970-01-01')), + createAsset(user1, new Date('1970-01-01')), + ]); const { status } = await request(server) .put('/asset') @@ -705,7 +1232,7 @@ describe(`${AssetController.name} (e2e)`, () => { }); it('should merge stack children', async () => { - const newParent = await createAsset(assetRepository, user1, defaultLibrary.id, new Date('1970-01-01')); + const newParent = await createAsset(user1, new Date('1970-01-01')); const { status } = await request(server) .put('/asset') .set('Authorization', `Bearer ${user1.accessToken}`) @@ -788,4 +1315,55 @@ describe(`${AssetController.name} (e2e)`, () => { expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: asset3.id })])); }); }); + + describe(AssetRepository.name, () => { + describe('getWithout', () => { + describe('WithoutProperty.FACES', () => { + const getAssetIdsWithoutFaces = async () => { + const assetPagination = usePagination(10, (pagination) => + assetRepository.getWithout(pagination, WithoutProperty.FACES), + ); + let assets: AssetEntity[] = []; + for await (const assetsPage of assetPagination) { + assets = [...assets, ...assetsPage]; + } + return assets.map((a) => a.id); + }; + + beforeEach(async () => { + await assetRepository.save({ id: asset1.id, resizePath: '/path/to/resize' }); + expect(await getAssetIdsWithoutFaces()).toContain(asset1.id); + }); + + describe('with recognized faces', () => { + beforeEach(async () => { + const personRepository = app.get(IPersonRepository); + const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' }); + await personRepository.createFace({ assetId: asset1.id, personId: person.id }); + }); + + it('should not return asset with facesRecognizedAt unset', async () => { + expect(await getAssetIdsWithoutFaces()).not.toContain(asset1.id); + }); + + it('should not return asset with facesRecognizedAt set', async () => { + await assetRepository.upsertJobStatus({ assetId: asset1.id, facesRecognizedAt: new Date() }); + expect(await getAssetIdsWithoutFaces()).not.toContain(asset1.id); + }); + }); + + describe('without recognized faces', () => { + it('should return asset with facesRecognizedAt unset', async () => { + expect(await getAssetIdsWithoutFaces()).toContain(asset1.id); + }); + + it('should not return asset with facesRecognizedAt set', async () => { + expect(await getAssetIdsWithoutFaces()).toContain(asset1.id); + await assetRepository.upsertJobStatus({ assetId: asset1.id, facesRecognizedAt: new Date() }); + expect(await getAssetIdsWithoutFaces()).not.toContain(asset1.id); + }); + }); + }); + }); + }); }); diff --git a/server/test/e2e/auth.e2e-spec.ts b/server/test/e2e/auth.e2e-spec.ts index 985645129..f7ab84772 100644 --- a/server/test/e2e/auth.e2e-spec.ts +++ b/server/test/e2e/auth.e2e-spec.ts @@ -1,6 +1,5 @@ import { AuthController } from '@app/immich'; import { api } from '@test/api'; -import { db } from '@test/db'; import { adminSignupStub, changePasswordStub, @@ -8,17 +7,33 @@ import { errorStub, loginResponseStub, loginStub, - signupResponseStub, uuidStub, } from '@test/fixtures'; import { testApp } from '@test/test-utils'; import request from 'supertest'; -const firstName = 'Immich'; -const lastName = 'Admin'; +const name = 'Immich Admin'; const password = 'Password123'; const email = 'admin@immich.app'; +const adminSignupResponse = { + avatarColor: expect.any(String), + id: expect.any(String), + name: 'Immich Admin', + email: 'admin@immich.app', + storageLabel: 'admin', + externalPath: null, + profileImagePath: '', + // why? lol + shouldChangePassword: true, + isAdmin: true, + createdAt: expect.any(String), + updatedAt: expect.any(String), + deletedAt: null, + oauthId: '', + memoriesEnabled: true, +}; + describe(`${AuthController.name} (e2e)`, () => { let server: any; let accessToken: string; @@ -33,7 +48,7 @@ describe(`${AuthController.name} (e2e)`, () => { }); beforeEach(async () => { - await db.reset(); + await testApp.reset(); await api.authApi.adminSignUp(server); const response = await api.authApi.adminLogin(server); accessToken = response.accessToken; @@ -41,29 +56,25 @@ describe(`${AuthController.name} (e2e)`, () => { describe('POST /auth/admin-sign-up', () => { beforeEach(async () => { - await db.reset(); + await testApp.reset(); }); const invalid = [ { should: 'require an email address', - data: { firstName, lastName, password }, + data: { name, password }, }, { should: 'require a password', - data: { firstName, lastName, email }, + data: { name, email }, }, { - should: 'require a first name ', - data: { lastName, email, password }, - }, - { - should: 'require a last name ', - data: { firstName, email, password }, + should: 'require a name', + data: { email, password }, }, { should: 'require a valid email', - data: { firstName, lastName, email: 'immich', password }, + data: { name, email: 'immich', password }, }, ]; @@ -84,7 +95,7 @@ describe(`${AuthController.name} (e2e)`, () => { .post('/auth/admin-sign-up') .send({ ...adminSignupStub, email: 'admin@local' }); expect(status).toEqual(201); - expect(body).toEqual({ ...signupResponseStub, email: 'admin@local' }); + expect(body).toEqual({ ...adminSignupResponse, email: 'admin@local' }); }); it('should transform email to lower case', async () => { @@ -92,7 +103,7 @@ describe(`${AuthController.name} (e2e)`, () => { .post('/auth/admin-sign-up') .send({ ...adminSignupStub, email: 'aDmIn@IMMICH.app' }); expect(status).toEqual(201); - expect(body).toEqual(signupResponseStub); + expect(body).toEqual(adminSignupResponse); }); it('should not allow a second admin to sign up', async () => { diff --git a/server/test/e2e/formats.e2e-spec.ts b/server/test/e2e/formats.e2e-spec.ts index f2fce83ac..40f43a92a 100644 --- a/server/test/e2e/formats.e2e-spec.ts +++ b/server/test/e2e/formats.e2e-spec.ts @@ -1,7 +1,7 @@ import { LoginResponseDto } from '@app/domain'; import { AssetType, LibraryType } from '@app/infra/entities'; import { api } from '@test/api'; -import { IMMICH_TEST_ASSET_PATH, db, runAllTests, testApp } from '@test/test-utils'; +import { IMMICH_TEST_ASSET_PATH, runAllTests, testApp } from '@test/test-utils'; describe(`Supported file formats (e2e)`, () => { let server: any; @@ -176,7 +176,7 @@ describe(`Supported file formats (e2e)`, () => { }); beforeEach(async () => { - await db.reset(); + await testApp.reset(); await api.authApi.adminSignUp(server); admin = await api.authApi.adminLogin(server); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); diff --git a/server/test/e2e/library.e2e-spec.ts b/server/test/e2e/library.e2e-spec.ts index 9cfbe8961..92c604e00 100644 --- a/server/test/e2e/library.e2e-spec.ts +++ b/server/test/e2e/library.e2e-spec.ts @@ -2,30 +2,16 @@ import { LibraryResponseDto, LoginResponseDto } from '@app/domain'; import { LibraryController } from '@app/immich'; import { AssetType, LibraryType } from '@app/infra/entities'; import { api } from '@test/api'; -import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, db, restoreTempFolder, testApp } from '@test/test-utils'; +import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, restoreTempFolder, testApp } from '@test/test-utils'; import * as fs from 'fs'; import request from 'supertest'; import { utimes } from 'utimes'; -import { errorStub, uuidStub } from '../fixtures'; +import { errorStub, userDto, uuidStub } from '../fixtures'; describe(`${LibraryController.name} (e2e)`, () => { let server: any; let admin: LoginResponseDto; - const user1Dto = { - email: 'user1@immich.app', - password: 'Password123', - firstName: 'User 1', - lastName: 'Test', - }; - - const user2Dto = { - email: 'user2@immich.app', - password: 'Password123', - firstName: 'User 2', - lastName: 'Test', - }; - beforeAll(async () => { [server] = await testApp.create({ jobs: true }); }); @@ -36,7 +22,7 @@ describe(`${LibraryController.name} (e2e)`, () => { }); beforeEach(async () => { - await db.reset(); + await testApp.reset(); await restoreTempFolder(); await api.authApi.adminSignUp(server); admin = await api.authApi.adminLogin(server); @@ -197,8 +183,8 @@ describe(`${LibraryController.name} (e2e)`, () => { }); it('should allow a user to create a library', async () => { - await api.userApi.create(server, admin.accessToken, user1Dto); - const user1 = await api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }); + await api.userApi.create(server, admin.accessToken, userDto.user1); + const user1 = await api.authApi.login(server, userDto.user1); const { status, body } = await request(server) .post('/library') @@ -339,11 +325,15 @@ describe(`${LibraryController.name} (e2e)`, () => { }); it("should not allow getting another user's library", async () => { - await api.userApi.create(server, admin.accessToken, user1Dto); - const user1 = await api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }); + await Promise.all([ + api.userApi.create(server, admin.accessToken, userDto.user1), + api.userApi.create(server, admin.accessToken, userDto.user2), + ]); - await api.userApi.create(server, admin.accessToken, user2Dto); - const user2 = await api.authApi.login(server, { email: user2Dto.email, password: user2Dto.password }); + const [user1, user2] = await Promise.all([ + api.authApi.login(server, userDto.user1), + api.authApi.login(server, userDto.user2), + ]); const library = await api.libraryApi.create(server, user1.accessToken, { type: LibraryType.EXTERNAL }); diff --git a/server/test/e2e/oauth.e2e-spec.ts b/server/test/e2e/oauth.e2e-spec.ts index 879d53815..b2c997076 100644 --- a/server/test/e2e/oauth.e2e-spec.ts +++ b/server/test/e2e/oauth.e2e-spec.ts @@ -1,6 +1,5 @@ import { OAuthController } from '@app/immich'; import { api } from '@test/api'; -import { db } from '@test/db'; import { errorStub } from '@test/fixtures'; import { testApp } from '@test/test-utils'; import request from 'supertest'; @@ -17,15 +16,11 @@ describe(`${OAuthController.name} (e2e)`, () => { }); beforeEach(async () => { - await db.reset(); + await testApp.reset(); await api.authApi.adminSignUp(server); }); describe('POST /oauth/authorize', () => { - beforeEach(async () => { - await db.reset(); - }); - it(`should throw an error if a redirect uri is not provided`, async () => { const { status, body } = await request(server).post('/oauth/authorize').send({}); expect(status).toBe(400); diff --git a/server/test/e2e/partner.e2e-spec.ts b/server/test/e2e/partner.e2e-spec.ts index 82a09dcc8..512491b69 100644 --- a/server/test/e2e/partner.e2e-spec.ts +++ b/server/test/e2e/partner.e2e-spec.ts @@ -1,61 +1,45 @@ -import { IPartnerRepository, LoginResponseDto, PartnerDirection } from '@app/domain'; +import { LoginResponseDto, PartnerDirection } from '@app/domain'; import { PartnerController } from '@app/immich'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; -import { db } from '@test/db'; -import { errorStub } from '@test/fixtures'; +import { errorStub, userDto } from '@test/fixtures'; import { testApp } from '@test/test-utils'; import request from 'supertest'; -const user1Dto = { - email: 'user1@immich.app', - password: 'Password123', - firstName: 'User 1', - lastName: 'Test', -}; - -const user2Dto = { - email: 'user2@immich.app', - password: 'Password123', - firstName: 'User 2', - lastName: 'Test', -}; - describe(`${PartnerController.name} (e2e)`, () => { - let app: INestApplication; let server: any; - let loginResponse: LoginResponseDto; - let accessToken: string; - let repository: IPartnerRepository; let user1: LoginResponseDto; let user2: LoginResponseDto; + let user3: LoginResponseDto; beforeAll(async () => { - [server, app] = await testApp.create(); - repository = app.get(IPartnerRepository); + [server] = await testApp.create(); + + await testApp.reset(); + await api.authApi.adminSignUp(server); + const admin = await api.authApi.adminLogin(server); + + await Promise.all([ + api.userApi.create(server, admin.accessToken, userDto.user1), + api.userApi.create(server, admin.accessToken, userDto.user2), + api.userApi.create(server, admin.accessToken, userDto.user3), + ]); + + [user1, user2, user3] = await Promise.all([ + api.authApi.login(server, userDto.user1), + api.authApi.login(server, userDto.user2), + api.authApi.login(server, userDto.user3), + ]); + + await Promise.all([ + api.partnerApi.create(server, user1.accessToken, user2.userId), + api.partnerApi.create(server, user2.accessToken, user1.userId), + ]); }); afterAll(async () => { await testApp.teardown(); }); - beforeEach(async () => { - await db.reset(); - await api.authApi.adminSignUp(server); - loginResponse = await api.authApi.adminLogin(server); - accessToken = loginResponse.accessToken; - - await Promise.all([ - api.userApi.create(server, accessToken, user1Dto), - api.userApi.create(server, accessToken, user2Dto), - ]); - - [user1, user2] = await Promise.all([ - api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }), - api.authApi.login(server, { email: user2Dto.email, password: user2Dto.password }), - ]); - }); - describe('GET /partner', () => { it('should require authentication', async () => { const { status, body } = await request(server).get('/partner'); @@ -65,7 +49,6 @@ describe(`${PartnerController.name} (e2e)`, () => { }); it('should get all partners shared by user', async () => { - await repository.create({ sharedById: user1.userId, sharedWithId: user2.userId }); const { status, body } = await request(server) .get('/partner') .set('Authorization', `Bearer ${user1.accessToken}`) @@ -76,7 +59,6 @@ describe(`${PartnerController.name} (e2e)`, () => { }); it('should get all partners that share with user', async () => { - await repository.create({ sharedById: user2.userId, sharedWithId: user1.userId }); const { status, body } = await request(server) .get('/partner') .set('Authorization', `Bearer ${user1.accessToken}`) @@ -89,7 +71,7 @@ describe(`${PartnerController.name} (e2e)`, () => { describe('POST /partner/:id', () => { it('should require authentication', async () => { - const { status, body } = await request(server).post(`/partner/${user2.userId}`); + const { status, body } = await request(server).post(`/partner/${user3.userId}`); expect(status).toBe(401); expect(body).toEqual(errorStub.unauthorized); @@ -97,15 +79,14 @@ describe(`${PartnerController.name} (e2e)`, () => { it('should share with new partner', async () => { const { status, body } = await request(server) - .post(`/partner/${user2.userId}`) + .post(`/partner/${user3.userId}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(201); - expect(body).toEqual(expect.objectContaining({ id: user2.userId })); + expect(body).toEqual(expect.objectContaining({ id: user3.userId })); }); it('should not share with new partner if already sharing with this partner', async () => { - await repository.create({ sharedById: user1.userId, sharedWithId: user2.userId }); const { status, body } = await request(server) .post(`/partner/${user2.userId}`) .set('Authorization', `Bearer ${user1.accessToken}`); @@ -115,18 +96,36 @@ describe(`${PartnerController.name} (e2e)`, () => { }); }); + describe('PUT /partner/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(server).put(`/partner/${user2.userId}`); + + expect(status).toBe(401); + expect(body).toEqual(errorStub.unauthorized); + }); + + it('should update partner', async () => { + const { status, body } = await request(server) + .put(`/partner/${user2.userId}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ inTimeline: false }); + + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ id: user2.userId, inTimeline: false })); + }); + }); + describe('DELETE /partner/:id', () => { it('should require authentication', async () => { - const { status, body } = await request(server).delete(`/partner/${user2.userId}`); + const { status, body } = await request(server).delete(`/partner/${user3.userId}`); expect(status).toBe(401); expect(body).toEqual(errorStub.unauthorized); }); it('should delete partner', async () => { - await repository.create({ sharedById: user1.userId, sharedWithId: user2.userId }); const { status } = await request(server) - .delete(`/partner/${user2.userId}`) + .delete(`/partner/${user3.userId}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); @@ -134,7 +133,7 @@ describe(`${PartnerController.name} (e2e)`, () => { it('should throw a bad request if partner not found', async () => { const { status, body } = await request(server) - .delete(`/partner/${user2.userId}`) + .delete(`/partner/${user3.userId}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(400); diff --git a/server/test/e2e/person.e2e-spec.ts b/server/test/e2e/person.e2e-spec.ts index 298e4418e..e3796619e 100644 --- a/server/test/e2e/person.e2e-spec.ts +++ b/server/test/e2e/person.e2e-spec.ts @@ -3,7 +3,6 @@ import { PersonController } from '@app/immich'; import { PersonEntity } from '@app/infra/entities'; import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; -import { db } from '@test/db'; import { errorStub, uuidStub } from '@test/fixtures'; import { testApp } from '@test/test-utils'; import request from 'supertest'; @@ -27,7 +26,7 @@ describe(`${PersonController.name}`, () => { }); beforeEach(async () => { - await db.reset(); + await testApp.reset(); await api.authApi.adminSignUp(server); loginResponse = await api.authApi.adminLogin(server); accessToken = loginResponse.accessToken; diff --git a/server/test/e2e/server-info.e2e-spec.ts b/server/test/e2e/server-info.e2e-spec.ts index 33b7c5693..cc1c8defa 100644 --- a/server/test/e2e/server-info.e2e-spec.ts +++ b/server/test/e2e/server-info.e2e-spec.ts @@ -1,31 +1,29 @@ import { LoginResponseDto } from '@app/domain'; import { ServerInfoController } from '@app/immich'; import { api } from '@test/api'; -import { db } from '@test/db'; -import { errorStub } from '@test/fixtures'; +import { errorStub, userDto } from '@test/fixtures'; import { testApp } from '@test/test-utils'; import request from 'supertest'; describe(`${ServerInfoController.name} (e2e)`, () => { let server: any; - let accessToken: string; - let loginResponse: LoginResponseDto; + let admin: LoginResponseDto; + let nonAdmin: LoginResponseDto; beforeAll(async () => { [server] = await testApp.create(); + + await testApp.reset(); + await api.authApi.adminSignUp(server); + admin = await api.authApi.adminLogin(server); + await api.userApi.create(server, admin.accessToken, userDto.user1); + nonAdmin = await api.authApi.login(server, userDto.user1); }); afterAll(async () => { await testApp.teardown(); }); - beforeEach(async () => { - await db.reset(); - await api.authApi.adminSignUp(server); - loginResponse = await api.authApi.adminLogin(server); - accessToken = loginResponse.accessToken; - }); - describe('GET /server-info', () => { it('should require authentication', async () => { const { status, body } = await request(server).get('/server-info'); @@ -34,7 +32,9 @@ describe(`${ServerInfoController.name} (e2e)`, () => { }); it('should return the disk information', async () => { - const { status, body } = await request(server).get('/server-info').set('Authorization', `Bearer ${accessToken}`); + const { status, body } = await request(server) + .get('/server-info') + .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ diskAvailable: expect.any(String), @@ -96,7 +96,6 @@ describe(`${ServerInfoController.name} (e2e)`, () => { expect(body).toEqual({ loginPageMessage: '', oauthButtonText: 'Login with OAuth', - mapTileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', trashDays: 30, isInitialized: true, }); @@ -111,12 +110,9 @@ describe(`${ServerInfoController.name} (e2e)`, () => { }); it('should only work for admins', async () => { - const loginDto = { email: 'test@immich.app', password: 'Immich123' }; - await api.userApi.create(server, accessToken, { ...loginDto, firstName: 'test', lastName: 'test' }); - const { accessToken: userAccessToken } = await api.authApi.login(server, loginDto); const { status, body } = await request(server) .get('/server-info/statistics') - .set('Authorization', `Bearer ${userAccessToken}`); + .set('Authorization', `Bearer ${nonAdmin.accessToken}`); expect(status).toBe(403); expect(body).toEqual(errorStub.forbidden); }); @@ -124,7 +120,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => { it('should return the server stats', async () => { const { status, body } = await request(server) .get('/server-info/statistics') - .set('Authorization', `Bearer ${accessToken}`); + .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ photos: 0, @@ -133,9 +129,15 @@ describe(`${ServerInfoController.name} (e2e)`, () => { { photos: 0, usage: 0, - userFirstName: 'Immich', - userId: loginResponse.userId, - userLastName: 'Admin', + userName: 'Immich Admin', + userId: admin.userId, + videos: 0, + }, + { + photos: 0, + usage: 0, + userName: 'User 1', + userId: nonAdmin.userId, videos: 0, }, ], diff --git a/server/test/e2e/shared-link.e2e-spec.ts b/server/test/e2e/shared-link.e2e-spec.ts index 03eb9da7d..787af1bfe 100644 --- a/server/test/e2e/shared-link.e2e-spec.ts +++ b/server/test/e2e/shared-link.e2e-spec.ts @@ -1,49 +1,113 @@ -import { AlbumResponseDto, LoginResponseDto, SharedLinkResponseDto } from '@app/domain'; -import { PartnerController } from '@app/immich'; -import { LibraryType, SharedLinkType } from '@app/infra/entities'; +import { + AlbumResponseDto, + AssetResponseDto, + IAssetRepository, + LoginResponseDto, + SharedLinkResponseDto, +} from '@app/domain'; +import { SharedLinkController } from '@app/immich'; +import { SharedLinkType } from '@app/infra/entities'; +import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; -import { db } from '@test/db'; -import { errorStub, uuidStub } from '@test/fixtures'; -import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, restoreTempFolder, testApp } from '@test/test-utils'; -import { cp } from 'fs/promises'; +import { errorStub, userDto, uuidStub } from '@test/fixtures'; +import { testApp } from '@test/test-utils'; +import { DateTime } from 'luxon'; import request from 'supertest'; -const user1Dto = { - email: 'user1@immich.app', - password: 'Password123', - firstName: 'User 1', - lastName: 'Test', -}; - -describe(`${PartnerController.name} (e2e)`, () => { +describe(`${SharedLinkController.name} (e2e)`, () => { let server: any; let admin: LoginResponseDto; + let asset1: AssetResponseDto; + let asset2: AssetResponseDto; let user1: LoginResponseDto; + let user2: LoginResponseDto; let album: AlbumResponseDto; - let sharedLink: SharedLinkResponseDto; + let metadataAlbum: AlbumResponseDto; + let deletedAlbum: AlbumResponseDto; + let linkWithDeletedAlbum: SharedLinkResponseDto; + let linkWithPassword: SharedLinkResponseDto; + let linkWithAlbum: SharedLinkResponseDto; + let linkWithAssets: SharedLinkResponseDto; + let linkWithMetadata: SharedLinkResponseDto; + let linkWithoutMetadata: SharedLinkResponseDto; + let app: INestApplication; beforeAll(async () => { - [server] = await testApp.create({ jobs: true }); + [server, app] = await testApp.create(); + const assetRepository = app.get(IAssetRepository); + + await testApp.reset(); + await api.authApi.adminSignUp(server); + admin = await api.authApi.adminLogin(server); + + await Promise.all([ + api.userApi.create(server, admin.accessToken, userDto.user1), + api.userApi.create(server, admin.accessToken, userDto.user2), + ]); + + [user1, user2] = await Promise.all([ + api.authApi.login(server, userDto.user1), + api.authApi.login(server, userDto.user2), + ]); + + [asset1, asset2] = await Promise.all([ + api.assetApi.create(server, user1.accessToken), + api.assetApi.create(server, user1.accessToken), + ]); + + await assetRepository.upsertExif({ + assetId: asset1.id, + longitude: -108.400968333333, + latitude: 39.115, + orientation: '1', + dateTimeOriginal: DateTime.fromISO('2022-01-10T19:15:44.310Z').toJSDate(), + timeZone: 'UTC-4', + state: 'Mesa County, Colorado', + country: 'United States of America', + }); + + [album, deletedAlbum, metadataAlbum] = await Promise.all([ + api.albumApi.create(server, user1.accessToken, { albumName: 'album' }), + api.albumApi.create(server, user2.accessToken, { albumName: 'deleted album' }), + api.albumApi.create(server, user1.accessToken, { albumName: 'metadata album', assetIds: [asset1.id] }), + ]); + + [linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] = + await Promise.all([ + api.sharedLinkApi.create(server, user2.accessToken, { + type: SharedLinkType.ALBUM, + albumId: deletedAlbum.id, + }), + api.sharedLinkApi.create(server, user1.accessToken, { + type: SharedLinkType.ALBUM, + albumId: album.id, + }), + api.sharedLinkApi.create(server, user1.accessToken, { + type: SharedLinkType.INDIVIDUAL, + assetIds: [asset1.id], + }), + api.sharedLinkApi.create(server, user1.accessToken, { + type: SharedLinkType.ALBUM, + albumId: album.id, + password: 'foo', + }), + api.sharedLinkApi.create(server, user1.accessToken, { + type: SharedLinkType.ALBUM, + albumId: metadataAlbum.id, + showMetadata: true, + }), + api.sharedLinkApi.create(server, user1.accessToken, { + type: SharedLinkType.ALBUM, + albumId: metadataAlbum.id, + showMetadata: false, + }), + ]); + + await api.userApi.delete(server, admin.accessToken, user2.userId); }); afterAll(async () => { await testApp.teardown(); - await restoreTempFolder(); - }); - - beforeEach(async () => { - await db.reset(); - await api.authApi.adminSignUp(server); - admin = await api.authApi.adminLogin(server); - - await api.userApi.create(server, admin.accessToken, user1Dto); - user1 = await api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }); - - album = await api.albumApi.create(server, user1.accessToken, { albumName: 'shared with link' }); - sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, - albumId: album.id, - }); }); describe('GET /shared-link', () => { @@ -60,7 +124,16 @@ describe(`${PartnerController.name} (e2e)`, () => { .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); - expect(body).toEqual([expect.objectContaining({ album, userId: user1.userId, type: SharedLinkType.ALBUM })]); + expect(body).toHaveLength(5); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: linkWithAlbum.id }), + expect.objectContaining({ id: linkWithAssets.id }), + expect.objectContaining({ id: linkWithPassword.id }), + expect.objectContaining({ id: linkWithMetadata.id }), + expect.objectContaining({ id: linkWithoutMetadata.id }), + ]), + ); }); it('should not get shared links created by other users', async () => { @@ -83,67 +156,92 @@ describe(`${PartnerController.name} (e2e)`, () => { }); it('should get data for correct shared link', async () => { - const { status, body } = await request(server).get('/shared-link/me').query({ key: sharedLink.key }); + const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithAlbum.key }); expect(status).toBe(200); - expect(body).toEqual(expect.objectContaining({ album, userId: user1.userId, type: SharedLinkType.ALBUM })); + expect(body).toEqual( + expect.objectContaining({ + album, + userId: user1.userId, + type: SharedLinkType.ALBUM, + }), + ); }); it('should return unauthorized for incorrect shared link', async () => { const { status, body } = await request(server) .get('/shared-link/me') - .query({ key: sharedLink.key + 'foo' }); + .query({ key: linkWithAlbum.key + 'foo' }); expect(status).toBe(401); expect(body).toEqual(errorStub.invalidShareKey); }); it('should return unauthorized if target has been soft deleted', async () => { - const softDeletedAlbum = await api.albumApi.create(server, user1.accessToken, { albumName: 'shared with link' }); - const softDeletedAlbumLink = await api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, - albumId: softDeletedAlbum.id, - }); - await api.userApi.delete(server, admin.accessToken, user1.userId); - - const { status, body } = await request(server).get('/shared-link/me').query({ key: softDeletedAlbumLink.key }); + const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithDeletedAlbum.key }); expect(status).toBe(401); expect(body).toEqual(errorStub.invalidShareKey); }); it('should return unauthorized for password protected link', async () => { - const passwordProtectedLink = await api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, - albumId: album.id, - password: 'foo', - }); - - const { status, body } = await request(server).get('/shared-link/me').query({ key: passwordProtectedLink.key }); + const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithPassword.key }); expect(status).toBe(401); expect(body).toEqual(errorStub.invalidSharePassword); }); it('should get data for correct password protected link', async () => { - const passwordProtectedLink = await api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, - albumId: album.id, - password: 'foo', - }); - const { status, body } = await request(server) .get('/shared-link/me') - .query({ key: passwordProtectedLink.key, password: 'foo' }); + .query({ key: linkWithPassword.key, password: 'foo' }); expect(status).toBe(200); expect(body).toEqual(expect.objectContaining({ album, userId: user1.userId, type: SharedLinkType.ALBUM })); }); + + it('should return metadata for album shared link', async () => { + const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithMetadata.key }); + + expect(status).toBe(200); + expect(body.assets).toHaveLength(1); + expect(body.assets[0]).toEqual( + expect.objectContaining({ + originalFileName: 'example', + localDateTime: expect.any(String), + fileCreatedAt: expect.any(String), + exifInfo: expect.objectContaining({ + longitude: -108.400968333333, + latitude: 39.115, + orientation: '1', + dateTimeOriginal: expect.any(String), + timeZone: 'UTC-4', + state: 'Mesa County, Colorado', + country: 'United States of America', + }), + }), + ); + expect(body.album).toBeDefined(); + }); + + it('should not return metadata for album shared link without metadata', async () => { + const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithoutMetadata.key }); + + expect(status).toBe(200); + expect(body.assets).toHaveLength(1); + expect(body.album).toBeDefined(); + + const asset = body.assets[0]; + expect(asset).not.toHaveProperty('exifInfo'); + expect(asset).not.toHaveProperty('fileCreatedAt'); + expect(asset).not.toHaveProperty('originalFilename'); + expect(asset).not.toHaveProperty('originalPath'); + }); }); describe('GET /shared-link/:id', () => { it('should require authentication', async () => { - const { status, body } = await request(server).get(`/shared-link/${sharedLink.id}`); + const { status, body } = await request(server).get(`/shared-link/${linkWithAlbum.id}`); expect(status).toBe(401); expect(body).toEqual(errorStub.unauthorized); @@ -151,7 +249,7 @@ describe(`${PartnerController.name} (e2e)`, () => { it('should get shared link by id', async () => { const { status, body } = await request(server) - .get(`/shared-link/${sharedLink.id}`) + .get(`/shared-link/${linkWithAlbum.id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); @@ -160,7 +258,7 @@ describe(`${PartnerController.name} (e2e)`, () => { it('should not get shared link by id if user has not created the link or it does not exist', async () => { const { status, body } = await request(server) - .get(`/shared-link/${sharedLink.id}`) + .get(`/shared-link/${linkWithAlbum.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); @@ -221,7 +319,7 @@ describe(`${PartnerController.name} (e2e)`, () => { describe('PATCH /shared-link/:id', () => { it('should require authentication', async () => { const { status, body } = await request(server) - .patch(`/shared-link/${sharedLink.id}`) + .patch(`/shared-link/${linkWithAlbum.id}`) .send({ description: 'foo' }); expect(status).toBe(401); @@ -240,7 +338,7 @@ describe(`${PartnerController.name} (e2e)`, () => { it('should update shared link', async () => { const { status, body } = await request(server) - .patch(`/shared-link/${sharedLink.id}`) + .patch(`/shared-link/${linkWithAlbum.id}`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ description: 'foo' }); @@ -251,9 +349,53 @@ describe(`${PartnerController.name} (e2e)`, () => { }); }); + describe('PUT /shared-link/:id/assets', () => { + it('should not add assets to shared link (album)', async () => { + const { status, body } = await request(server) + .put(`/shared-link/${linkWithAlbum.id}/assets`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ assetIds: [asset2.id] }); + + expect(status).toBe(400); + expect(body).toEqual(errorStub.badRequest('Invalid shared link type')); + }); + + it('should add an assets to a shared link (individual)', async () => { + const { status, body } = await request(server) + .put(`/shared-link/${linkWithAssets.id}/assets`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ assetIds: [asset2.id] }); + + expect(body).toEqual([{ assetId: asset2.id, success: true }]); + expect(status).toBe(200); + }); + }); + + describe('DELETE /shared-link/:id/assets', () => { + it('should not remove assets from a shared link (album)', async () => { + const { status, body } = await request(server) + .delete(`/shared-link/${linkWithAlbum.id}/assets`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ assetIds: [asset2.id] }); + + expect(status).toBe(400); + expect(body).toEqual(errorStub.badRequest('Invalid shared link type')); + }); + + it('should remove assets from a shared link (individual)', async () => { + const { status, body } = await request(server) + .delete(`/shared-link/${linkWithAssets.id}/assets`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ assetIds: [asset2.id] }); + + expect(body).toEqual([{ assetId: asset2.id, success: true }]); + expect(status).toBe(200); + }); + }); + describe('DELETE /shared-link/:id', () => { it('should require authentication', async () => { - const { status, body } = await request(server).delete(`/shared-link/${sharedLink.id}`); + const { status, body } = await request(server).delete(`/shared-link/${linkWithAlbum.id}`); expect(status).toBe(401); expect(body).toEqual(errorStub.unauthorized); @@ -268,89 +410,12 @@ describe(`${PartnerController.name} (e2e)`, () => { expect(body).toEqual(errorStub.badRequest()); }); - it('should update shared link', async () => { + it('should delete a shared link', async () => { const { status } = await request(server) - .delete(`/shared-link/${sharedLink.id}`) + .delete(`/shared-link/${linkWithAlbum.id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); }); }); - - describe('Shared link metadata', () => { - beforeEach(async () => { - await restoreTempFolder(); - - await cp( - `${IMMICH_TEST_ASSET_PATH}/metadata/gps-position/thompson-springs.jpg`, - `${IMMICH_TEST_ASSET_TEMP_PATH}/thompson-springs.jpg`, - ); - - await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); - - const library = await api.libraryApi.create(server, admin.accessToken, { - type: LibraryType.EXTERNAL, - importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], - }); - - await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); - - const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - - expect(assets).toHaveLength(1); - - album = await api.albumApi.create(server, admin.accessToken, { albumName: 'New album' }); - await api.albumApi.addAssets(server, admin.accessToken, album.id, { ids: [assets[0].id] }); - }); - - it('should return metadata for album shared link', async () => { - const sharedLink = await api.sharedLinkApi.create(server, admin.accessToken, { - type: SharedLinkType.ALBUM, - albumId: album.id, - }); - - const returnedLink = await api.sharedLinkApi.getMySharedLink(server, sharedLink.key); - - expect(returnedLink.assets).toHaveLength(1); - expect(returnedLink.album).toBeDefined(); - - const returnedAsset = returnedLink.assets[0]; - expect(returnedAsset).toEqual( - expect.objectContaining({ - originalFileName: 'thompson-springs', - resized: true, - localDateTime: '2022-01-10T15:15:44.310Z', - fileCreatedAt: '2022-01-10T19:15:44.310Z', - exifInfo: expect.objectContaining({ - longitude: -108.400968333333, - latitude: 39.115, - orientation: '1', - dateTimeOriginal: '2022-01-10T19:15:44.310Z', - timeZone: 'UTC-4', - state: 'Mesa County, Colorado', - country: 'United States of America', - }), - }), - ); - }); - - it('should not return metadata for album shared link without metadata', async () => { - const sharedLink = await api.sharedLinkApi.create(server, admin.accessToken, { - type: SharedLinkType.ALBUM, - albumId: album.id, - showMetadata: false, - }); - - const returnedLink = await api.sharedLinkApi.getMySharedLink(server, sharedLink.key); - - expect(returnedLink.assets).toHaveLength(1); - expect(returnedLink.album).toBeDefined(); - - const returnedAsset = returnedLink.assets[0]; - expect(returnedAsset).not.toHaveProperty('exifInfo'); - expect(returnedAsset).not.toHaveProperty('fileCreatedAt'); - expect(returnedAsset).not.toHaveProperty('originalFilename'); - expect(returnedAsset).not.toHaveProperty('originalPath'); - }); - }); }); diff --git a/server/test/e2e/system-config.e2e-spec.ts b/server/test/e2e/system-config.e2e-spec.ts new file mode 100644 index 000000000..15ed54da3 --- /dev/null +++ b/server/test/e2e/system-config.e2e-spec.ts @@ -0,0 +1,72 @@ +import { LoginResponseDto } from '@app/domain'; +import { SystemConfigController } from '@app/immich'; +import { api } from '@test/api'; +import { errorStub, userDto } from '@test/fixtures'; +import { testApp } from '@test/test-utils'; +import request from 'supertest'; + +describe(`${SystemConfigController.name} (e2e)`, () => { + let server: any; + let admin: LoginResponseDto; + let nonAdmin: LoginResponseDto; + + beforeAll(async () => { + [server] = await testApp.create(); + + await testApp.reset(); + await api.authApi.adminSignUp(server); + admin = await api.authApi.adminLogin(server); + await api.userApi.create(server, admin.accessToken, userDto.user1); + nonAdmin = await api.authApi.login(server, userDto.user1); + }); + + afterAll(async () => { + await testApp.teardown(); + }); + + describe('GET /system-config/map/style.json', () => { + it('should require authentication', async () => { + const { status, body } = await request(server).get('/system-config/map/style.json'); + expect(status).toBe(401); + expect(body).toEqual(errorStub.unauthorized); + }); + + it('should throw an error if a theme is not light or dark', async () => { + for (const theme of ['dark1', true, 123, '', null, undefined]) { + const { status, body } = await request(server) + .get('/system-config/map/style.json') + .query({ theme }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorStub.badRequest(['theme must be one of the following values: light, dark'])); + } + }); + + it('should return the light style.json', async () => { + const { status, body } = await request(server) + .get('/system-config/map/style.json') + .query({ theme: 'light' }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ id: 'immich-map-light' })); + }); + + it('should return the dark style.json', async () => { + const { status, body } = await request(server) + .get('/system-config/map/style.json') + .query({ theme: 'dark' }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' })); + }); + + it('should not require admin authentication', async () => { + const { status, body } = await request(server) + .get('/system-config/map/style.json') + .query({ theme: 'dark' }) + .set('Authorization', `Bearer ${nonAdmin.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' })); + }); + }); +}); diff --git a/server/test/e2e/user.e2e-spec.ts b/server/test/e2e/user.e2e-spec.ts index 02b03e9ba..41f393b7d 100644 --- a/server/test/e2e/user.e2e-spec.ts +++ b/server/test/e2e/user.e2e-spec.ts @@ -4,8 +4,7 @@ import { UserEntity } from '@app/infra/entities'; import { INestApplication } from '@nestjs/common'; import { getRepositoryToken } from '@nestjs/typeorm'; import { api } from '@test/api'; -import { db } from '@test/db'; -import { errorStub, userSignupStub, userStub } from '@test/fixtures'; +import { errorStub, userDto, userSignupStub, userStub } from '@test/fixtures'; import { testApp } from '@test/test-utils'; import request from 'supertest'; import { Repository } from 'typeorm'; @@ -28,7 +27,7 @@ describe(`${UserController.name}`, () => { }); beforeEach(async () => { - await db.reset(); + await testApp.reset(); await api.authApi.adminSignUp(server); loginResponse = await api.authApi.adminLogin(server); accessToken = loginResponse.accessToken; @@ -36,11 +35,6 @@ describe(`${UserController.name}`, () => { userService = app.get(UserService); }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - }); - describe('GET /user', () => { it('should require authentication', async () => { const { status, body } = await request(server).get('/user'); @@ -59,8 +53,7 @@ describe(`${UserController.name}`, () => { const user1 = await api.userApi.create(server, accessToken, { email: `user1@immich.app`, password: 'Password123', - firstName: `User 1`, - lastName: 'Test', + name: `User 1`, }); await api.userApi.delete(server, accessToken, user1.id); @@ -75,12 +68,7 @@ describe(`${UserController.name}`, () => { }); it('should include deleted users', async () => { - const user1 = await api.userApi.create(server, accessToken, { - email: `user1@immich.app`, - password: 'Password123', - firstName: `User 1`, - lastName: 'Test', - }); + const user1 = await api.userApi.create(server, accessToken, userDto.user1); await api.userApi.delete(server, accessToken, user1.id); @@ -88,6 +76,7 @@ describe(`${UserController.name}`, () => { .get(`/user`) .query({ isAll: false }) .set('Authorization', `Bearer ${accessToken}`); + expect(status).toBe(200); expect(body).toHaveLength(2); expect(body[0]).toMatchObject({ id: user1.id, email: 'user1@immich.app', deletedAt: expect.any(String) }); @@ -149,8 +138,7 @@ describe(`${UserController.name}`, () => { isAdmin: true, email: 'user1@immich.app', password: 'Password123', - firstName: 'Immich', - lastName: 'User', + name: 'Immich', }) .set('Authorization', `Bearer ${accessToken}`); expect(body).toMatchObject({ @@ -167,8 +155,7 @@ describe(`${UserController.name}`, () => { .send({ email: 'no-memories@immich.app', password: 'Password123', - firstName: 'No Memories', - lastName: 'User', + name: 'No Memories', memoriesEnabled: false, }) .set('Authorization', `Bearer ${accessToken}`); @@ -186,8 +173,7 @@ describe(`${UserController.name}`, () => { beforeEach(async () => { userToDelete = await api.userApi.create(server, accessToken, { email: userStub.user1.email, - firstName: userStub.user1.firstName, - lastName: userStub.user1.lastName, + name: userStub.user1.name, password: 'superSecurePassword', }); }); @@ -246,8 +232,7 @@ describe(`${UserController.name}`, () => { const user = await api.userApi.create(server, accessToken, { email: 'user1@immich.app', password: 'Password123', - firstName: 'Immich', - lastName: 'User', + name: 'Immich User', }); const { status, body } = await request(server) @@ -284,15 +269,13 @@ describe(`${UserController.name}`, () => { const before = await api.userApi.get(server, accessToken, loginResponse.userId); const after = await api.userApi.update(server, accessToken, { id: before.id, - firstName: 'First Name', - lastName: 'Last Name', + name: 'Name', }); expect(after).toEqual({ ...before, updatedAt: expect.any(String), - firstName: 'First Name', - lastName: 'Last Name', + name: 'Name', }); expect(before.updatedAt).not.toEqual(after.updatedAt); }); diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts index 48ed92817..fd4464d19 100644 --- a/server/test/fixtures/album.stub.ts +++ b/server/test/fixtures/album.stub.ts @@ -18,6 +18,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), sharedWithUser: Object.freeze({ id: 'album-2', @@ -33,6 +34,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [userStub.user1], + isActivityEnabled: true, }), sharedWithMultiple: Object.freeze({ id: 'album-3', @@ -48,6 +50,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [userStub.user1, userStub.user2], + isActivityEnabled: true, }), sharedWithAdmin: Object.freeze({ id: 'album-3', @@ -63,6 +66,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [userStub.admin], + isActivityEnabled: true, }), oneAsset: Object.freeze({ id: 'album-4', @@ -78,6 +82,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), twoAssets: Object.freeze({ id: 'album-4a', @@ -93,6 +98,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), emptyWithInvalidThumbnail: Object.freeze({ id: 'album-5', @@ -108,6 +114,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), emptyWithValidThumbnail: Object.freeze({ id: 'album-5', @@ -123,6 +130,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), oneAssetInvalidThumbnail: Object.freeze({ id: 'album-6', @@ -138,6 +146,7 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), oneAssetValidThumbnail: Object.freeze({ id: 'album-6', @@ -153,5 +162,6 @@ export const albumStub = { deletedAt: null, sharedLinks: [], sharedUsers: [], + isActivityEnabled: true, }), }; diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts index 6a45c16af..d4b636896 100644 --- a/server/test/fixtures/auth.stub.ts +++ b/server/test/fixtures/auth.stub.ts @@ -1,8 +1,7 @@ import { AuthUserDto } from '@app/domain'; export const adminSignupStub = { - firstName: 'Immich', - lastName: 'Admin', + name: 'Immich Admin', email: 'admin@immich.app', password: 'Password123', }; @@ -12,14 +11,6 @@ export const userSignupStub = { memoriesEnabled: true, }; -export const signupResponseStub = { - id: expect.any(String), - email: 'admin@immich.app', - firstName: 'Immich', - lastName: 'Admin', - createdAt: expect.any(String), -}; - export const loginStub = { admin: { email: 'admin@immich.app', @@ -111,9 +102,8 @@ export const loginResponseStub = { admin: { response: { accessToken: expect.any(String), - firstName: 'Immich', + name: 'Immich Admin', isAdmin: true, - lastName: 'Admin', profileImagePath: '', shouldChangePassword: true, userEmail: 'admin@immich.app', @@ -125,8 +115,7 @@ export const loginResponseStub = { accessToken: 'cmFuZG9tLWJ5dGVz', userId: 'user-id', userEmail: 'immich@test.com', - firstName: 'immich_first_name', - lastName: 'immich_last_name', + name: 'immich_name', profileImagePath: '', isAdmin: false, shouldChangePassword: false, @@ -141,8 +130,7 @@ export const loginResponseStub = { accessToken: 'cmFuZG9tLWJ5dGVz', userId: 'user-id', userEmail: 'immich@test.com', - firstName: 'immich_first_name', - lastName: 'immich_last_name', + name: 'immich_name', profileImagePath: '', isAdmin: false, shouldChangePassword: false, @@ -157,8 +145,7 @@ export const loginResponseStub = { accessToken: 'cmFuZG9tLWJ5dGVz', userId: 'user-id', userEmail: 'immich@test.com', - firstName: 'immich_first_name', - lastName: 'immich_last_name', + name: 'immich_name', profileImagePath: '', isAdmin: false, shouldChangePassword: false, diff --git a/server/test/fixtures/partner.stub.ts b/server/test/fixtures/partner.stub.ts index dc54332ea..05c1c67d6 100644 --- a/server/test/fixtures/partner.stub.ts +++ b/server/test/fixtures/partner.stub.ts @@ -9,6 +9,7 @@ export const partnerStub = { sharedBy: userStub.admin, sharedWith: userStub.user1, sharedWithId: userStub.user1.id, + inTimeline: true, }), user1ToAdmin1: Object.freeze({ createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -17,5 +18,6 @@ export const partnerStub = { sharedById: userStub.user1.id, sharedWithId: userStub.admin.id, sharedWith: userStub.admin, + inTimeline: true, }), }; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index dd6eb5233..56a0c1045 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -100,6 +100,7 @@ const albumResponse: AlbumResponseDto = { hasSharedLink: false, assets: [], assetCount: 1, + isActivityEnabled: true, }; export const sharedLinkStub = { @@ -179,6 +180,7 @@ export const sharedLinkStub = { albumThumbnailAssetId: null, sharedUsers: [], sharedLinks: [], + isActivityEnabled: true, assets: [ { id: 'id_1', diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index af5515baf..9a03579af 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -1,12 +1,29 @@ -import { UserEntity } from '@app/infra/entities'; +import { UserAvatarColor, UserEntity } from '@app/infra/entities'; import { authStub } from './auth.stub'; +export const userDto = { + user1: { + email: 'user1@immich.app', + password: 'Password123', + name: 'User 1', + }, + user2: { + email: 'user2@immich.app', + password: 'Password123', + name: 'User 2', + }, + user3: { + email: 'user3@immich.app', + password: 'Password123', + name: 'User 3', + }, +}; + export const userStub = { admin: Object.freeze({ ...authStub.admin, password: 'admin_password', - firstName: 'admin_first_name', - lastName: 'admin_last_name', + name: 'admin_name', storageLabel: 'admin', externalPath: null, oauthId: '', @@ -18,12 +35,12 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), user1: Object.freeze({ ...authStub.user1, password: 'immich_password', - firstName: 'immich_first_name', - lastName: 'immich_last_name', + name: 'immich_name', storageLabel: null, externalPath: null, oauthId: '', @@ -35,12 +52,12 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), user2: Object.freeze({ ...authStub.user2, password: 'immich_password', - firstName: 'immich_first_name', - lastName: 'immich_last_name', + name: 'immich_name', storageLabel: null, externalPath: null, oauthId: '', @@ -52,12 +69,12 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), storageLabel: Object.freeze({ ...authStub.user1, password: 'immich_password', - firstName: 'immich_first_name', - lastName: 'immich_last_name', + name: 'immich_name', storageLabel: 'label-1', externalPath: null, oauthId: '', @@ -69,12 +86,12 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), externalPath1: Object.freeze({ ...authStub.user1, password: 'immich_password', - firstName: 'immich_first_name', - lastName: 'immich_last_name', + name: 'immich_name', storageLabel: 'label-1', externalPath: '/data/user1', oauthId: '', @@ -86,12 +103,12 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), externalPath2: Object.freeze({ ...authStub.user1, password: 'immich_password', - firstName: 'immich_first_name', - lastName: 'immich_last_name', + name: 'immich_name', storageLabel: 'label-1', externalPath: '/data/user2', oauthId: '', @@ -103,12 +120,12 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), profilePath: Object.freeze({ ...authStub.user1, password: 'immich_password', - firstName: 'immich_first_name', - lastName: 'immich_last_name', + name: 'immich_name', storageLabel: 'label-1', externalPath: null, oauthId: '', @@ -120,5 +137,6 @@ export const userStub = { tags: [], assets: [], memoriesEnabled: true, + avatarColor: UserAvatarColor.PRIMARY, }), }; diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index 4f7992e86..4c2a5ed8d 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -8,6 +8,7 @@ export interface IAccessRepositoryMock { library: jest.Mocked; timeline: jest.Mocked; person: jest.Mocked; + partner: jest.Mocked; } export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => { @@ -19,35 +20,41 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => activity: { hasOwnerAccess: jest.fn(), hasAlbumOwnerAccess: jest.fn(), + hasCreateAccess: jest.fn(), }, + asset: { - hasOwnerAccess: jest.fn(), - hasAlbumAccess: jest.fn(), - hasPartnerAccess: jest.fn(), - hasSharedLinkAccess: jest.fn(), + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), + checkAlbumAccess: jest.fn().mockResolvedValue(new Set()), + checkPartnerAccess: jest.fn().mockResolvedValue(new Set()), + checkSharedLinkAccess: jest.fn().mockResolvedValue(new Set()), }, album: { - hasOwnerAccess: jest.fn(), - hasSharedAlbumAccess: jest.fn(), - hasSharedLinkAccess: jest.fn(), + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), + checkSharedAlbumAccess: jest.fn().mockResolvedValue(new Set()), + checkSharedLinkAccess: jest.fn().mockResolvedValue(new Set()), }, authDevice: { - hasOwnerAccess: jest.fn(), + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), }, library: { - hasOwnerAccess: jest.fn(), - hasPartnerAccess: jest.fn(), + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), + checkPartnerAccess: jest.fn().mockResolvedValue(new Set()), }, timeline: { - hasPartnerAccess: jest.fn(), + checkPartnerAccess: jest.fn().mockResolvedValue(new Set()), }, person: { - hasOwnerAccess: jest.fn(), + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), + }, + + partner: { + checkUpdateAccess: jest.fn().mockResolvedValue(new Set()), }, }; }; diff --git a/server/test/repositories/album.repository.mock.ts b/server/test/repositories/album.repository.mock.ts index 7cd0a846b..36c3afb29 100644 --- a/server/test/repositories/album.repository.mock.ts +++ b/server/test/repositories/album.repository.mock.ts @@ -5,7 +5,7 @@ export const newAlbumRepositoryMock = (): jest.Mocked => { getById: jest.fn(), getByIds: jest.fn(), getByAssetId: jest.fn(), - getAssetCountForIds: jest.fn(), + getMetadataForIds: jest.fn(), getInvalidThumbnail: jest.fn(), getOwned: jest.fn(), getShared: jest.fn(), diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index c185d3983..88bbdabcf 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -4,6 +4,7 @@ export const newAssetRepositoryMock = (): jest.Mocked => { return { create: jest.fn(), upsertExif: jest.fn(), + upsertJobStatus: jest.fn(), getByDate: jest.fn(), getByDayOfYear: jest.fn(), getByIds: jest.fn().mockResolvedValue([]), @@ -17,6 +18,7 @@ export const newAssetRepositoryMock = (): jest.Mocked => { getFirstAssetForAlbumId: jest.fn(), getLastUpdatedAssetForAlbumId: jest.fn(), getAll: jest.fn().mockResolvedValue({ items: [], hasNextPage: false }), + getAllByDeviceId: jest.fn(), updateAll: jest.fn(), getByLibraryId: jest.fn(), getByLibraryIdAndOriginalPath: jest.fn(), @@ -30,5 +32,6 @@ export const newAssetRepositoryMock = (): jest.Mocked => { getTimeBuckets: jest.fn(), restoreAll: jest.fn(), softDeleteAll: jest.fn(), + search: jest.fn(), }; }; diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts index 76c6f777a..3e97cb327 100644 --- a/server/test/repositories/metadata.repository.mock.ts +++ b/server/test/repositories/metadata.repository.mock.ts @@ -2,10 +2,10 @@ import { IMetadataRepository } from '@app/domain'; export const newMetadataRepositoryMock = (): jest.Mocked => { return { - deleteCache: jest.fn(), - getExifTags: jest.fn(), init: jest.fn(), teardown: jest.fn(), reverseGeocode: jest.fn(), + readTags: jest.fn(), + writeTags: jest.fn(), }; }; diff --git a/server/test/repositories/partner.repository.mock.ts b/server/test/repositories/partner.repository.mock.ts index a283325d2..1e839ae4f 100644 --- a/server/test/repositories/partner.repository.mock.ts +++ b/server/test/repositories/partner.repository.mock.ts @@ -6,5 +6,6 @@ export const newPartnerRepositoryMock = (): jest.Mocked => { remove: jest.fn(), getAll: jest.fn(), get: jest.fn(), + update: jest.fn(), }; }; diff --git a/server/test/repositories/system-config.repository.mock.ts b/server/test/repositories/system-config.repository.mock.ts index 44ad8f630..3be69f267 100644 --- a/server/test/repositories/system-config.repository.mock.ts +++ b/server/test/repositories/system-config.repository.mock.ts @@ -6,6 +6,7 @@ export const newSystemConfigRepositoryMock = (reset = true): jest.Mocked[]; +} export const db = { - reset: async () => { + reset: async (options?: ResetOptions) => { if (!dataSource.isInitialized) { await dataSource.initialize(); } await dataSource.transaction(async (em) => { - for (const entity of dataSource.entityMetadatas) { - if (entity.tableName === 'users') { + const entities = options?.entities || []; + const tableNames = + entities.length > 0 + ? entities.map((entity) => em.getRepository(entity).metadata.tableName) + : dataSource.entityMetadatas + .map((entity) => entity.tableName) + .filter((tableName) => !tableName.startsWith('geodata')); + + let deleteUsers = false; + for (const tableName of tableNames) { + if (tableName === 'users') { + deleteUsers = true; continue; } - await em.query(`DELETE FROM ${entity.tableName} CASCADE;`); + await em.query(`DELETE FROM ${tableName} CASCADE;`); + } + if (deleteUsers) { + await em.query(`DELETE FROM "users" CASCADE;`); } - await em.query(`DELETE FROM "users" CASCADE;`); }); }, disconnect: async () => { @@ -71,8 +87,8 @@ export const testApp = { return [app.getHttpServer(), app]; }, - reset: async () => { - await db.reset(); + reset: async (options?: ResetOptions) => { + await db.reset(options); }, teardown: async () => { await app.get(AppService).teardown(); diff --git a/web/.dockerignore b/web/.dockerignore index 834ab88b6..3b05277d1 100644 --- a/web/.dockerignore +++ b/web/.dockerignore @@ -1,4 +1,4 @@ node_modules/ -upload/ -dist/ - +coverage/ +.svelte-kit +build/ diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs index fdf66b700..9277676ac 100644 --- a/web/.eslintrc.cjs +++ b/web/.eslintrc.cjs @@ -35,5 +35,6 @@ module.exports = { varsIgnorePattern: '^_$', }, ], + curly: 2, }, }; diff --git a/web/.prettierrc b/web/.prettierrc index fb0e0c92b..33325428a 100644 --- a/web/.prettierrc +++ b/web/.prettierrc @@ -3,7 +3,6 @@ "trailingComma": "all", "printWidth": 120, "semi": true, - "organizeImportsSkipDestructiveCodeActions": true, "plugins": ["prettier-plugin-svelte"], - "pluginSearchDirs": false + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] } diff --git a/web/Dockerfile b/web/Dockerfile index d6d58217e..28d18db55 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,44 +1,11 @@ -# Our Node base image -FROM node:20.8-alpine3.18 as base +FROM node:iron-alpine3.18 +USER node WORKDIR /usr/src/app -EXPOSE 3000 -RUN apk add --no-cache setpriv tini - -FROM base as builder - -RUN chown node:node /usr/src/app - COPY --chown=node:node package*.json ./ - RUN npm ci - COPY --chown=node:node . . - -EXPOSE 3000 - -FROM builder AS dev ENV CHOKIDAR_USEPOLLING=true EXPOSE 24678 +EXPOSE 3000 CMD ["npm", "run", "dev"] - -FROM builder AS prod - -RUN npm run build -RUN npm prune --omit=dev - -FROM base - -ENV NODE_ENV=production - -WORKDIR /usr/src/app - -COPY --from=prod /usr/src/app/node_modules ./node_modules -COPY --from=prod /usr/src/app/build ./build - -COPY package.json package-lock.json ./ -COPY entrypoint.sh ./ - -ENTRYPOINT ["tini", "--", "/bin/sh"] - -CMD ["entrypoint.sh"] diff --git a/web/LICENSE b/web/LICENSE deleted file mode 100644 index a72f39880..000000000 --- a/web/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Hau Tran - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/web/entrypoint.sh b/web/entrypoint.sh deleted file mode 100755 index 5f52b7b5f..000000000 --- a/web/entrypoint.sh +++ /dev/null @@ -1,11 +0,0 @@ -#! /bin/sh - -# Rebind env vars to PUBLIC_ for svelte -export PUBLIC_IMMICH_SERVER_URL=$IMMICH_SERVER_URL -export PUBLIC_IMMICH_API_URL_EXTERNAL=$IMMICH_API_URL_EXTERNAL - -if [ "$(id -u)" -eq 0 ] && [ -n "$PUID" ] && [ -n "$PGID" ]; then - exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups node /usr/src/app/build/index.js -else - exec node /usr/src/app/build/index.js -fi diff --git a/web/package-lock.json b/web/package-lock.json index a79f648de..0eef10223 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,7 +1,7 @@ { "name": "immich-web", "version": "1.0.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -10,57 +10,53 @@ "dependencies": { "@egjs/svelte-view360": "^4.0.0-beta.7", "@mdi/js": "^7.3.67", - "@zoom-image/svelte": "^0.1.8", + "@zoom-image/svelte": "^0.2.0", "axios": "^0.27.2", "buffer": "^6.0.3", "copy-image-clipboard": "^2.1.2", "dom-to-image": "^2.6.0", "handlebars": "^4.7.7", "justified-layout": "^4.1.0", - "leaflet": "^1.9.4", - "leaflet.markercluster": "^1.5.3", "lodash-es": "^4.17.21", "luxon": "^3.2.1", + "maplibre-gl": "^3.6.0", "socket.io-client": "^4.6.1", "svelte-loading-spinners": "^0.3.4", - "svelte-local-storage-store": "^0.5.0", + "svelte-local-storage-store": "^0.6.0", + "svelte-maplibre": "^0.7.0", "thumbhash": "^0.1.1" }, "devDependencies": { "@babel/preset-env": "^7.20.2", "@babel/preset-typescript": "^7.22.5", - "@faker-js/faker": "^7.6.0", + "@faker-js/faker": "^8.0.0", "@floating-ui/dom": "^1.5.1", - "@sveltejs/adapter-node": "^1.2.0", + "@sveltejs/adapter-static": "^2.0.3", "@sveltejs/kit": "^1.20.4", - "@testing-library/jest-dom": "^5.16.5", + "@testing-library/jest-dom": "^6.0.0", "@testing-library/svelte": "^4.0.3", - "@types/cookie": "^0.5.1", "@types/dom-to-image": "^2.6.4", "@types/justified-layout": "^4.1.0", - "@types/leaflet": "^1.9.1", - "@types/leaflet.markercluster": "^1.5.1", "@types/lodash-es": "^4.17.6", "@types/luxon": "^3.2.0", - "@typescript-eslint/eslint-plugin": "^5.53.0", - "@typescript-eslint/parser": "^5.53.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", "autoprefixer": "^10.4.13", "babel-jest": "^29.4.3", "eslint": "^8.34.0", - "eslint-config-prettier": "^8.6.0", + "eslint-config-prettier": "^9.0.0", "eslint-plugin-svelte": "^2.30.0", "factory.ts": "^1.3.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.4.3", "jest-environment-jsdom": "^29.4.3", "postcss": "^8.4.21", - "prettier": "^2.8.4", - "prettier-plugin-svelte": "^2.10.1", + "prettier": "^3.1.0", + "prettier-plugin-svelte": "^3.1.2", "svelte": "^4.0.5", "svelte-check": "^3.4.3", - "svelte-jester": "^2.3.2", + "svelte-jester": "^3.0.0", "svelte-preprocess": "^5.0.3", - "tailwind-merge": "^1.14.0", "tailwindcss": "^3.2.7", "tslib": "^2.5.0", "typescript": "^5.0.0", @@ -120,9 +116,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", - "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", + "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -254,9 +250,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", - "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", + "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -328,9 +324,9 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", - "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -519,9 +515,9 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", - "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", + "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -534,14 +530,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", - "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", + "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.15" + "@babel/plugin-transform-optional-chaining": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -550,6 +546,22 @@ "@babel/core": "^7.13.0" } }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.3.tgz", + "integrity": "sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -638,9 +650,9 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", + "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -653,9 +665,9 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -692,9 +704,9 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -809,9 +821,9 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", - "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", + "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -840,9 +852,9 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -855,14 +867,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", - "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.3.tgz", + "integrity": "sha512-59GsVNavGxAXCDDbakWSMJhajASb4kBCqDjqJsv+p5nKdbz7istmZ3HrX3L2LuiI80+zsOADCvooqQH3qGCucQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.9", + "@babel/helper-remap-async-to-generator": "^7.22.20", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { @@ -873,14 +885,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" + "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -890,9 +902,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", + "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -905,9 +917,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", - "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.3.tgz", + "integrity": "sha512-QPZxHrThbQia7UdvfpaRRlq/J9ciz1J4go0k+lPBXbgaNeY7IQrBj/9ceWjvMMI07/ZBzHl/F0R/2K0qH7jCVw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -920,12 +932,12 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", + "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -936,12 +948,12 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", - "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.3.tgz", + "integrity": "sha512-PENDVxdr7ZxKPyi5Ffc0LjXdnJyrJxyqF5T5YjlVg4a0VFfQHW0r8iAtRiDXkfHlu1wwcvdtnndGYIeJLSuRMQ==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, @@ -953,18 +965,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", - "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.3.tgz", + "integrity": "sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, @@ -976,13 +988,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", + "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" + "@babel/template": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -992,9 +1004,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", - "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", + "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1007,12 +1019,12 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", + "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1023,9 +1035,9 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", + "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1038,9 +1050,9 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", - "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.3.tgz", + "integrity": "sha512-vTG+cTGxPFou12Rj7ll+eD5yWeNl5/8xvQvF08y5Gv3v4mZQoyFf8/n9zg4q5vvCWt5jmgymfzMAldO7orBn7A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1054,12 +1066,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", + "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1070,9 +1082,9 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", - "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.3.tgz", + "integrity": "sha512-yCLhW34wpJWRdTxxWtFZASJisihrfyMOTOQexhVzA78jlU+dH7Dw+zQgcPepQ5F3C6bAIiblZZ+qBggJdHiBAg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1086,9 +1098,9 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", - "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.3.tgz", + "integrity": "sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1101,13 +1113,13 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", + "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1118,9 +1130,9 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", - "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.3.tgz", + "integrity": "sha512-H9Ej2OiISIZowZHaBwF0tsJOih1PftXJtE8EWqlEIwpc7LMTGq0rPOrywKLQ4nefzx8/HMR0D3JGXoMHYvhi0A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1134,9 +1146,9 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", + "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1149,9 +1161,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", - "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.3.tgz", + "integrity": "sha512-+pD5ZbxofyOygEp+zZAfujY2ShNCXRpDRIPOiBmTO693hhyOEteZgl876Xs9SAHPQpcV0vz8LvA/T+w8AzyX8A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1165,9 +1177,9 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", + "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1180,12 +1192,12 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", - "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", + "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1196,12 +1208,12 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", - "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", + "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, @@ -1213,13 +1225,13 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", - "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", + "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.0", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20" }, @@ -1231,12 +1243,12 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", + "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1263,9 +1275,9 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", + "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1278,9 +1290,9 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", - "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.3.tgz", + "integrity": "sha512-xzg24Lnld4DYIdysyf07zJ1P+iIfJpxtVFOzX4g+bsJ3Ng5Le7rXx9KwqKzuyaUeRnt+I1EICwQITqc0E2PmpA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1294,9 +1306,9 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", - "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.3.tgz", + "integrity": "sha512-s9GO7fIBi/BLsZ0v3Rftr6Oe4t0ctJ8h4CCXfPoEJwmvAPMyNrfkOOJzm6b9PX9YXcCJWWQd/sBF/N26eBiMVw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1310,16 +1322,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", - "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.3.tgz", + "integrity": "sha512-VxHt0ANkDmu8TANdE9Kc0rndo/ccsmfe2Cx2y5sI4hu3AukHQ5wAu4cM7j3ba8B9548ijVyclBU+nuDQftZsog==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", + "@babel/compat-data": "^7.23.3", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.15" + "@babel/plugin-transform-parameters": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1329,13 +1341,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", + "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" + "@babel/helper-replace-supers": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1345,9 +1357,9 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", - "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.3.tgz", + "integrity": "sha512-LxYSb0iLjUamfm7f1D7GpiS4j0UAC8AOiehnsGAP8BEsIX8EOi3qV6bbctw8M7ZvLtcoZfZX5Z7rN9PlWk0m5A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1361,9 +1373,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", - "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.3.tgz", + "integrity": "sha512-zvL8vIfIUgMccIAK1lxjvNv572JHFJIKb4MWBz5OGdBQA0fB0Xluix5rmOby48exiJc987neOmP/m9Fnpkz3Tg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1378,9 +1390,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", - "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", + "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1393,12 +1405,12 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", + "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1409,13 +1421,13 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", - "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.3.tgz", + "integrity": "sha512-a5m2oLNFyje2e/rGKjVfAELTVI5mbA0FeZpBnkOWWV7eSmKQ+T/XW0Vf+29ScLzSxX+rnsarvU0oie/4m6hkxA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.11", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, @@ -1427,9 +1439,9 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", + "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1442,9 +1454,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", - "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", + "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1458,9 +1470,9 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", + "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1473,9 +1485,9 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", + "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1488,9 +1500,9 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", + "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1504,9 +1516,9 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", + "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1519,9 +1531,9 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", + "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1534,9 +1546,9 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", + "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1549,15 +1561,15 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.15.tgz", - "integrity": "sha512-1uirS0TnijxvQLnlv5wQBwOX3E1wCFX7ITv+9pBV2wKEk4K+M5tqDaoNXnTH8tjEIYHLO98MwiTWO04Ggz4XuA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.3.tgz", + "integrity": "sha512-ogV0yWnq38CFwH20l2Afz0dfKuZBx9o/Y2Rmh5vuSS0YD1hswgEgTfyTzuSrT2q9btmHRSqYoSfwFUVaC1M1Jw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.22.5" + "@babel/plugin-syntax-typescript": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1567,9 +1579,9 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", - "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", + "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1582,12 +1594,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", + "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1598,12 +1610,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", + "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1614,12 +1626,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", + "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1630,25 +1642,26 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz", - "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.3.tgz", + "integrity": "sha512-ovzGc2uuyNfNAs/jyjIGxS8arOHS5FENZaNn4rtE7UdKMMkqHCvboHfcuhWLZNX5cB44QfcGNWjaevxMzzMf+Q==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.20", + "@babel/compat-data": "^7.23.3", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -1660,59 +1673,58 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.15", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.15", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.11", - "@babel/plugin-transform-classes": "^7.22.15", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.15", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.11", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.11", - "@babel/plugin-transform-for-of": "^7.22.15", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.11", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.15", - "@babel/plugin-transform-modules-systemjs": "^7.22.11", - "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.3", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.3", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.3", + "@babel/plugin-transform-classes": "^7.23.3", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.3", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.3", + "@babel/plugin-transform-for-of": "^7.23.3", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.3", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.3", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.3", + "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-numeric-separator": "^7.22.11", - "@babel/plugin-transform-object-rest-spread": "^7.22.15", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.22.15", - "@babel/plugin-transform-parameters": "^7.22.15", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.11", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.10", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.10", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.3", + "@babel/plugin-transform-numeric-separator": "^7.23.3", + "@babel/plugin-transform-object-rest-spread": "^7.23.3", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.3", + "@babel/plugin-transform-optional-chaining": "^7.23.3", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.3", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", "@babel/preset-modules": "0.1.6-no-external-plugins", - "@babel/types": "^7.22.19", - "babel-plugin-polyfill-corejs2": "^0.4.5", - "babel-plugin-polyfill-corejs3": "^0.8.3", - "babel-plugin-polyfill-regenerator": "^0.5.2", + "babel-plugin-polyfill-corejs2": "^0.4.6", + "babel-plugin-polyfill-corejs3": "^0.8.5", + "babel-plugin-polyfill-regenerator": "^0.5.3", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -1738,16 +1750,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.0.tgz", - "integrity": "sha512-6P6VVa/NM/VlAYj5s2Aq/gdVg8FSENCg3wlZ6Qau9AcPaoF5LbN1nyGlR9DTRIw9PpxI94e+ReydsJHcjwAweg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz", + "integrity": "sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/plugin-transform-typescript": "^7.22.15" + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-typescript": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1870,54 +1882,6 @@ "gl-matrix": "^3.4.3" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", @@ -1934,294 +1898,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2247,9 +1923,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -2315,22 +1991,28 @@ } }, "node_modules/@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@faker-js/faker": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", - "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.3.1.tgz", + "integrity": "sha512-FdgpFxY6V6rLZE9mmIBb9hM0xpfvQOSNOLnzolzKwsE1DH+gC7lEKV1p1IbR0lAYyvYd5a4u3qWJzowUkw1bIw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], "engines": { - "node": ">=14.0.0", - "npm": ">=6.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" } }, "node_modules/@fastify/busboy": { @@ -2368,12 +2050,12 @@ "dev": true }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -2395,9 +2077,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { @@ -3197,6 +2879,75 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz", + "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" + }, + "node_modules/@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "19.3.3", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.3.tgz", + "integrity": "sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^3.0.0", + "minimist": "^1.2.8", + "rw": "^1.3.3", + "sort-object": "^3.0.3" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, "node_modules/@mdi/js": { "version": "7.3.67", "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.3.67.tgz", @@ -3252,98 +3003,6 @@ "integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==", "dev": true }, - "node_modules/@rollup/plugin-commonjs": { - "version": "25.0.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.4.tgz", - "integrity": "sha512-L92Vz9WUZXDnlQQl3EwbypJR4+DM2EbsO+/KOcEkP4Mc6Ct453EeDB2uH9lgRwj4w5yflgNpq9pHOiY8aoUXBQ==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "glob": "^8.0.3", - "is-reference": "1.2.1", - "magic-string": "^0.27.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-json": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.0.0.tgz", - "integrity": "sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz", - "integrity": "sha512-nsbUg588+GDSu8/NS8T4UAshO6xeaOfINNuXeVHcKV02LJtoRaM1SiOacClw4kws1SFiNhdLGxlbMY9ga/zs/w==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-builtin-module": "^3.2.1", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.4.tgz", - "integrity": "sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3373,41 +3032,35 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, - "node_modules/@sveltejs/adapter-node": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-1.3.1.tgz", - "integrity": "sha512-A0VgRQDCDPzdLNoiAbcOxGw4zT1Mc+n1LwT1OmO350R7WxrEqdMUChPPOd1iMfIDWlP4ie6E2d/WQf5es2d4Zw==", + "node_modules/@sveltejs/adapter-static": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.3.tgz", + "integrity": "sha512-VUqTfXsxYGugCpMqQv1U0LIdbR3S5nBkMMDmpjGVJyM6Q2jHVMFtdWJCkeHMySc6mZxJ+0eZK3T7IgmUCDrcUQ==", "dev": true, - "dependencies": { - "@rollup/plugin-commonjs": "^25.0.0", - "@rollup/plugin-json": "^6.0.0", - "@rollup/plugin-node-resolve": "^15.0.1", - "rollup": "^3.7.0" - }, "peerDependencies": { - "@sveltejs/kit": "^1.0.0" + "@sveltejs/kit": "^1.5.0" } }, "node_modules/@sveltejs/kit": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.25.1.tgz", - "integrity": "sha512-pD8XsvNJNgTNkFngNlM60my/X8dXWPKVzN5RghEQr0NjGZmuCjy49AfFu2cGbZjNf5pBcqd2RCNMW912P5fkhA==", + "version": "1.27.6", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.27.6.tgz", + "integrity": "sha512-GsjTkMbKzXdbeRg0tk8S7HNShQ4879ftRr0ZHaZfjbig1xQwG57Bvcm9U9/mpLJtCapLbLWUnygKrgcLISLC8A==", "dev": true, "hasInstallScript": true, "dependencies": { - "@sveltejs/vite-plugin-svelte": "^2.4.1", + "@sveltejs/vite-plugin-svelte": "^2.5.0", "@types/cookie": "^0.5.1", "cookie": "^0.5.0", "devalue": "^4.3.1", "esm-env": "^1.0.0", "kleur": "^4.1.5", "magic-string": "^0.30.0", - "mime": "^3.0.0", + "mrmime": "^1.0.1", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^2.0.2", "tiny-glob": "^0.2.9", - "undici": "~5.25.0" + "undici": "~5.26.2" }, "bin": { "svelte-kit": "svelte-kit.js" @@ -3416,14 +3069,20 @@ "node": "^16.14 || >=18" }, "peerDependencies": { - "svelte": "^3.54.0 || ^4.0.0-next.0", + "svelte": "^3.54.0 || ^4.0.0-next.0 || ^5.0.0-next.0", "vite": "^4.0.0" } }, + "node_modules/@sveltejs/kit/node_modules/@types/cookie": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.4.tgz", + "integrity": "sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==", + "dev": true + }, "node_modules/@sveltejs/kit/node_modules/magic-string": { - "version": "0.30.4", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", - "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -3433,9 +3092,9 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.6.tgz", - "integrity": "sha512-zO79p0+DZnXPnF0ltIigWDx/ux7Ni+HRaFOw720Qeivc1azFUrJxTl0OryXVibYNx1hCboGia1NRV3x8RNv4cA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.5.2.tgz", + "integrity": "sha512-Dfy0Rbl+IctOVfJvWGxrX/3m6vxPLH8o0x+8FA5QEyMUQMo4kGOVIojjryU7YomBAexOTAuYf1RT7809yDziaA==", "dev": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^1.0.4", @@ -3450,7 +3109,7 @@ "node": "^14.18.0 || >= 16" }, "peerDependencies": { - "svelte": "^3.54.0 || ^4.0.0", + "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0-next.0", "vite": "^4.0.0" } }, @@ -3582,14 +3241,13 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", - "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.1.4.tgz", + "integrity": "sha512-wpoYrCYwSZ5/AxcrjLxJmCU6I5QAJXslEeSiMQqaWmP2Kzpd1LvF/qxmAIW2qposULGWq2gw30GgVNFLSc2Jnw==", "dev": true, "dependencies": { - "@adobe/css-tools": "^4.0.1", + "@adobe/css-tools": "^4.3.1", "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", @@ -3598,9 +3256,29 @@ "redent": "^3.0.0" }, "engines": { - "node": ">=8", + "node": ">=14", "npm": ">=6", "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } } }, "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { @@ -3671,9 +3349,9 @@ } }, "node_modules/@testing-library/svelte": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-4.0.3.tgz", - "integrity": "sha512-GldAnyGEOn5gMwME+hLVQrnfuKZFB+it5YOMnRBHX+nqeHMsSa18HeqkdvGqtqLpvn81xV7R7EYFb500ngUfXA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-4.0.5.tgz", + "integrity": "sha512-P7X3mpYv/My4hBZfxVxTFV5KcA+EoWfRCguWP7WQdYj6HMdg/L+LiwG4ocvLe+hupedrC7dwcy85JlxKplJp2g==", "dev": true, "dependencies": { "@testing-library/dom": "^9.3.1" @@ -3741,16 +3419,10 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/cookie": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.2.tgz", - "integrity": "sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA==", - "dev": true - }, "node_modules/@types/dom-to-image": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@types/dom-to-image/-/dom-to-image-2.6.5.tgz", - "integrity": "sha512-SMYQf4urvjHfSsaEMhIULyjfawUv2a92OfglcGF7dQdDHBjfnGPtWoxw6hmKMiwsdmowHn70mxLw+F5cA7Imyg==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/@types/dom-to-image/-/dom-to-image-2.6.7.tgz", + "integrity": "sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==", "dev": true }, "node_modules/@types/estree": { @@ -3759,10 +3431,9 @@ "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==" }, "node_modules/@types/geojson": { - "version": "7946.0.11", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.11.tgz", - "integrity": "sha512-L7A0AINMXQpVwxHJ4jxD6/XjZ4NDufaRlUJHjNIFKYUFBH1SvOW+neaqb0VTRSLW5suSrSu19ObFEFnfNcr+qg==", - "dev": true + "version": "7946.0.13", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.13.tgz", + "integrity": "sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ==" }, "node_modules/@types/graceful-fs": { "version": "4.1.7", @@ -3802,6 +3473,8 @@ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz", "integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -3812,6 +3485,8 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=10" }, @@ -3824,6 +3499,8 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -3837,7 +3514,9 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/@types/jsdom": { "version": "20.0.1", @@ -3851,35 +3530,17 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", - "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/justified-layout": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@types/justified-layout/-/justified-layout-4.1.1.tgz", - "integrity": "sha512-/ZJGVeDif6EHRzK3kUifyOekGJcBXD1s/eRYAYgkJHI4QAkohz62E0PSMbFrGpOdTulPWRgOAh1mFZbYw9a9iQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/justified-layout/-/justified-layout-4.1.4.tgz", + "integrity": "sha512-q2ybP0u0NVj87oMnGZOGxY2iUN8ddr48zPOBHBdbOLpsMTA/keGj+93ou+OMCnJk0xewzlNIaVEkxM6VBD3E2w==", "dev": true }, - "node_modules/@types/leaflet": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.6.tgz", - "integrity": "sha512-HakGTK5LBBWegNWsAmTlG55zN1zszYec7aG47/z6SzT90bW2vqjmbqk3YKAbrtveO+G7fSTKTYqVbIwAFnTrbg==", - "dev": true, - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/leaflet.markercluster": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.2.tgz", - "integrity": "sha512-Yfi5R0Fb0xc/qotTuqJDAX65XKRt6DauzIdsMTcgrrhgf8DpBOrqPV/jw8/zUjY8FetRd6QdnefNdNJKmG/+zA==", - "dev": true, - "dependencies": { - "@types/leaflet": "*" - } - }, "node_modules/@types/lodash": { "version": "4.14.199", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", @@ -3887,42 +3548,56 @@ "dev": true }, "node_modules/@types/lodash-es": { - "version": "4.17.9", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.9.tgz", - "integrity": "sha512-ZTcmhiI3NNU7dEvWLZJkzG6ao49zOIjEgIE0RgV7wbPxU0f2xT3VSAHw2gmst8swH6V0YkLRGp4qPlX/6I90MQ==", + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "dev": true, "dependencies": { "@types/lodash": "*" } }, "node_modules/@types/luxon": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz", - "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.5.tgz", + "integrity": "sha512-1cyf6Ge/94zlaWIZA2ei1pE6SZ8xpad2hXaYa5JEFiaUH0YS494CZwyi4MXNpXD9oEuv6ZH0Bmh0e7F9sPhmZA==", "dev": true }, + "node_modules/@types/mapbox__point-geometry": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==" + }, + "node_modules/@types/mapbox__vector-tile": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", + "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", + "dependencies": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, "node_modules/@types/node": { "version": "20.8.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", "dev": true }, + "node_modules/@types/pbf": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", + "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==" + }, "node_modules/@types/pug": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.7.tgz", "integrity": "sha512-I469DU0UXNC1aHepwirWhu9YKg5fkxohZD95Ey/5A7lovC+Siu+MCLffva87lnfThaOrw9Vb1DUN5t55oULAAw==", "dev": true }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true - }, "node_modules/@types/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/stack-utils": { @@ -3931,13 +3606,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, - "node_modules/@types/testing-library__jest-dom": { - "version": "5.14.9", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", - "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==", - "dev": true, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", "dependencies": { - "@types/jest": "*" + "@types/geojson": "*" } }, "node_modules/@types/tough-cookie": { @@ -3967,32 +3641,33 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", + "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/type-utils": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4034,25 +3709,26 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4061,16 +3737,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4078,25 +3754,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", + "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/utils": "6.13.1", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -4105,12 +3781,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4118,21 +3794,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4178,29 +3854,28 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "semver": "^7.5.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { @@ -4237,26 +3912,32 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@zoom-image/core": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.24.0.tgz", - "integrity": "sha512-sN2bkyFelUtV9v07Nvhm+V86hVouEGfqsRz0Grqox943vMDn16izS2yRK3ZHuqeyMskvSDxCcC65p8B15ieX+w==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.31.0.tgz", + "integrity": "sha512-lvFVfIe/CSASXVq1E2vWnt/inXqrBMgjW96lW/l1JdM9EaCj5yis6YXPL5z+Rz2WHmMg5bb7Ps6w1Gzs/bC8LQ==", "dependencies": { "@namnode/store": "^0.1.0" }, @@ -4266,11 +3947,11 @@ } }, "node_modules/@zoom-image/svelte": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.1.16.tgz", - "integrity": "sha512-HxcsBaAj7XcDYdVF1ynMtbayNWB/csG0Iwtg35Q1IPeoKj+W/jVNYHeuzJgxeNnPGnN7ZyeAe/OX8c8BFW9XJg==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.1.tgz", + "integrity": "sha512-UGOFsXJN5Sk/uJxp7ZMajedXusmdmQ23nTNgphR4T9Q0Aef4qJJZI5dpGZtMCbGH2kdLbpIm30Sbht9kIe1L1Q==", "dependencies": { - "@zoom-image/core": "0.24.0" + "@zoom-image/core": "0.31.0" }, "funding": { "type": "github", @@ -4431,6 +4112,14 @@ "dequal": "^2.0.3" } }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", @@ -4453,6 +4142,14 @@ "node": ">=8" } }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4647,13 +4344,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", - "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", + "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.2", + "@babel/helper-define-polyfill-provider": "^0.4.3", "semver": "^6.3.1" }, "peerDependencies": { @@ -4661,25 +4358,25 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.4.tgz", - "integrity": "sha512-9l//BZZsPR+5XjyJMPtZSK4jv0BsTO1zDac2GC6ygx9WLGlcsnRd1Co0B2zT5fF5Ic6BZy+9m3HNZ3QcOeDKfg==", + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", + "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.2", - "core-js-compat": "^3.32.2" + "@babel/helper-define-polyfill-provider": "^0.4.3", + "core-js-compat": "^3.33.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", - "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", + "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.2" + "@babel/helper-define-polyfill-provider": "^0.4.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -4859,16 +4556,21 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node_modules/bytewise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz", + "integrity": "sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==", + "dependencies": { + "bytewise-core": "^1.2.2", + "typewise": "^1.0.3" + } + }, + "node_modules/bytewise-core": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bytewise-core/-/bytewise-core-1.2.3.tgz", + "integrity": "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==", + "dependencies": { + "typewise-core": "^1.2" } }, "node_modules/call-bind": { @@ -5064,14 +4766,6 @@ "periscopic": "^3.1.0" } }, - "node_modules/code-red/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -5113,12 +4807,6 @@ "node": ">= 6" } }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5146,9 +4834,9 @@ "integrity": "sha512-3VCXVl2IpFfOyD8drv9DozcNlwmqBqxOlsgkEGyVAzadjlPk1go8YNZyy8QmTnwHPxSFpeCR9OdsStEdVK7qDA==" }, "node_modules/core-js-compat": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz", - "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==", + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.2.tgz", + "integrity": "sha512-axfo+wxFVxnqf8RvxTzoAlzW4gRoacrHeoFlc9n0x50+7BEyZL/Rt3hicaED1/CEd7I6tPCPVUYcJwCMO5XUYw==", "dev": true, "dependencies": { "browserslist": "^4.22.1" @@ -5317,6 +5005,28 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -5550,6 +5260,11 @@ "node": ">=12" } }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + }, "node_modules/electron-to-chromium": { "version": "1.4.538", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.538.tgz", @@ -5747,18 +5462,19 @@ } }, "node_modules/eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.54.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -5800,10 +5516,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-compat-utils": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz", + "integrity": "sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, "node_modules/eslint-config-prettier": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", - "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -5813,16 +5541,17 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "2.33.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.33.2.tgz", - "integrity": "sha512-knWmauax+E/jvQ9CmuX5dAhQKP9P4eGQZxWa5RMutEJVCcy0wFmiUvOeDND2jR4vUkbDlX4khKjaceY7QzbkYw==", + "version": "2.35.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.35.1.tgz", + "integrity": "sha512-IF8TpLnROSGy98Z3NrsKXWDSCbNY2ReHDcrYTuXZMbfX7VmESISR78TWgO9zdg4Dht1X8coub5jKwHzP0ExRug==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@jridgewell/sourcemap-codec": "^1.4.14", "debug": "^4.3.1", + "eslint-compat-utils": "^0.1.2", "esutils": "^2.0.3", - "known-css-properties": "^0.28.0", + "known-css-properties": "^0.29.0", "postcss": "^8.4.5", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^6.0.0", @@ -5879,19 +5608,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -6013,9 +5729,9 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -6180,20 +5896,13 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } }, "node_modules/esutils": { "version": "2.0.3", @@ -6252,6 +5961,17 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/factory.ts": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/factory.ts/-/factory.ts-1.4.1.tgz", @@ -6329,6 +6049,11 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.1.tgz", + "integrity": "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -6484,6 +6209,11 @@ "node": ">=6.9.0" } }, + "node_modules/geojson-vt": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", + "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -6521,7 +6251,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, "engines": { "node": ">=10" }, @@ -6529,30 +6258,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/gl-matrix": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" }, - "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6565,25 +6283,28 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" }, "engines": { - "node": ">=10" + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, "node_modules/globals": { @@ -6942,6 +6663,11 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -6956,6 +6682,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -7032,21 +6766,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", - "dev": true, - "dependencies": { - "builtin-modules": "^3.3.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -7086,6 +6805,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7134,12 +6861,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -7177,7 +6898,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, "dependencies": { "isobject": "^3.0.1" }, @@ -7192,10 +6912,9 @@ "dev": true }, "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", "dependencies": { "@types/estree": "*" } @@ -7325,14 +7044,12 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9475,6 +9192,11 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-pretty-compact": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz", + "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -9487,11 +9209,26 @@ "node": ">=6" } }, + "node_modules/just-compare": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/just-compare/-/just-compare-2.3.0.tgz", + "integrity": "sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg==" + }, + "node_modules/just-flush": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/just-flush/-/just-flush-2.3.0.tgz", + "integrity": "sha512-fBuxQ1gJ61BurmhwKS5LYTzhkbrT5j/2U7ax+UbLm9aRvCTh2h6AfzLteOckE4KKomqOf0Y3zIG3Xu57sRsKUg==" + }, "node_modules/justified-layout": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/justified-layout/-/justified-layout-4.1.0.tgz", "integrity": "sha512-M5FimNMXgiOYerVRGsXZ2YK9YNCaTtwtYp7Hb2308U1Q9TXXHx5G0p08mcVR5O53qf8bWY4NJcPBxE6zuayXSg==" }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==" + }, "node_modules/keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -9505,7 +9242,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9520,24 +9256,11 @@ } }, "node_modules/known-css-properties": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.28.0.tgz", - "integrity": "sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz", + "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==", "dev": true }, - "node_modules/leaflet": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", - "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" - }, - "node_modules/leaflet.markercluster": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", - "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", - "peerDependencies": { - "leaflet": "^1.3.1" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -9625,9 +9348,9 @@ } }, "node_modules/luxon": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", - "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", "engines": { "node": ">=12" } @@ -9710,6 +9433,45 @@ "tmpl": "1.0.5" } }, + "node_modules/maplibre-gl": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-3.6.2.tgz", + "integrity": "sha512-krg2KFIdOpLPngONDhP6ixCoWl5kbdMINP0moMSJFVX7wX1Clm2M9hlNKXS8vBGlVWwR5R3ZfI6IPrYz7c+aCQ==", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^19.3.3", + "@types/geojson": "^7946.0.13", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "earcut": "^2.2.4", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.4.3", + "global-prefix": "^3.0.0", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^2.0.0", + "quickselect": "^2.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.3" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -9743,18 +9505,6 @@ "node": ">=8.6" } }, - "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -9847,6 +9597,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -9882,12 +9637,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -10187,6 +9936,18 @@ "node": ">=8" } }, + "node_modules/pbf": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", + "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/periscopic": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", @@ -10197,22 +9958,6 @@ "is-reference": "^3.0.0" } }, - "node_modules/periscopic/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/periscopic/node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dependencies": { - "@types/estree": "*" - } - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -10261,6 +10006,14 @@ "node": ">=8" } }, + "node_modules/pmtiles": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-2.11.0.tgz", + "integrity": "sha512-dU9SzzaqmCGpdEuTnIba6bDHT6j09ZJFIXxwGpvkiEnce3ZnBB1VKt6+EOmJGueriweaZLAMTUmKVElU2CBe0g==", + "dependencies": { + "fflate": "^0.8.0" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -10434,6 +10187,11 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/potpack": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", + "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -10444,28 +10202,28 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/prettier-plugin-svelte": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-2.10.1.tgz", - "integrity": "sha512-Wlq7Z5v2ueCubWo0TZzKc9XHcm7TDxqcuzRuGd0gcENfzfT4JZ9yDlCbEgxWgiPmLHkBjfOtpAWkcT28MCDpUQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.1.2.tgz", + "integrity": "sha512-7xfMZtwgAWHMT0iZc8jN4o65zgbAQ3+O32V6W7pXrqNvKnHnkoyQCGCbKeUyXKZLbYE0YhFRnamfxfkEGxm8qA==", "dev": true, "peerDependencies": { - "prettier": "^1.16.4 || ^2.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0" + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "node_modules/pretty-format": { @@ -10516,6 +10274,11 @@ "node": ">=6" } }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -10573,6 +10336,11 @@ } ] }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -10754,6 +10522,14 @@ "node": ">=8" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -10847,6 +10623,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -10950,6 +10731,20 @@ "node": ">= 0.4" } }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -11073,6 +10868,38 @@ "sorcery": "bin/sorcery" } }, + "node_modules/sort-asc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz", + "integrity": "sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-desc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.2.0.tgz", + "integrity": "sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-object": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-3.0.3.tgz", + "integrity": "sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==", + "dependencies": { + "bytewise": "^1.1.0", + "get-value": "^2.0.2", + "is-extendable": "^0.1.1", + "sort-asc": "^0.2.0", + "sort-desc": "^0.2.0", + "union-value": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -11099,6 +10926,40 @@ "source-map": "^0.6.0" } }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -11261,6 +11122,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -11286,9 +11155,9 @@ } }, "node_modules/svelte": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.1.tgz", - "integrity": "sha512-LpLqY2Jr7cRxkrTc796/AaaoMLF/1ax7cto8Ot76wrvKQhrPmZ0JgajiWPmg9mTSDqO16SSLiD17r9MsvAPTmw==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.7.tgz", + "integrity": "sha512-UExR1KS7raTdycsUrKLtStayu4hpdV3VZQgM0akX8XbXgLBlosdE/Sf3crOgyh9xIjqSYB3UEBuUlIQKRQX2hg==", "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", @@ -11301,7 +11170,7 @@ "estree-walker": "^3.0.3", "is-reference": "^3.0.1", "locate-character": "^3.0.0", - "magic-string": "^0.30.0", + "magic-string": "^0.30.4", "periscopic": "^3.1.0" }, "engines": { @@ -11309,9 +11178,9 @@ } }, "node_modules/svelte-check": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.5.2.tgz", - "integrity": "sha512-5a/YWbiH4c+AqAUP+0VneiV5bP8YOk9JL3jwvN+k2PEPLgpu85bjQc5eE67+eIZBBwUEJzmO3I92OqKcqbp3fw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.2.tgz", + "integrity": "sha512-E6iFh4aUCGJLRz6QZXH3gcN/VFfkzwtruWSRmlKrLWQTiO6VzLsivR6q02WYLGNAGecV3EocqZuCDrC2uttZ0g==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", @@ -11320,14 +11189,14 @@ "import-fresh": "^3.2.1", "picocolors": "^1.0.0", "sade": "^1.7.4", - "svelte-preprocess": "^5.0.4", + "svelte-preprocess": "^5.1.0", "typescript": "^5.0.3" }, "bin": { "svelte-check": "bin/svelte-check" }, "peerDependencies": { - "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0" + "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" } }, "node_modules/svelte-eslint-parser": { @@ -11395,12 +11264,13 @@ } }, "node_modules/svelte-jester": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/svelte-jester/-/svelte-jester-2.3.2.tgz", - "integrity": "sha512-JtxSz4FWAaCRBXbPsh4LcDs4Ua7zdXgLC0TZvT1R56hRV0dymmNP+abw67DTPF7sQPyNxWsOKd0Sl7Q8SnP8kg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/svelte-jester/-/svelte-jester-3.0.0.tgz", + "integrity": "sha512-V279cL906++hn00hkL1xAr/y5OjjxPYWic1g0yTJFmqdbdWKthdcuP3XBvmmwP9AzFBT51DlPgXz56HItle1Ug==", "dev": true, "engines": { - "node": ">=14" + "node": ">=16", + "pnpm": "^8.0.0" }, "peerDependencies": { "jest": ">= 27", @@ -11413,20 +11283,49 @@ "integrity": "sha512-vKaW71QMCBcTNijAGc0mUl8k3DQ66iYmp6MB8BMGCXyWk82bTrcLy8FOnSm9fE+8q6TwzD6PLUoYFHt0II93Xw==" }, "node_modules/svelte-local-storage-store": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.5.0.tgz", - "integrity": "sha512-SEDrpapeia6fUqta+r1NvSLlJYPkZ4pBcl15EYIOSPNzy6vhpoXu8cnzUDmZxsWl7fZGAHxrVH9UyZCbyO4W+g==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.6.4.tgz", + "integrity": "sha512-45WoY2vSGPQM1sIQJ9jTkPPj20hYeqm+af6mUGRFSPP5WglZf36YYoZqwmZZ8Dt/2SU8lem+BTA8/Z/8TkqNLg==", "engines": { "node": ">=0.14" }, "peerDependencies": { - "svelte": "^3.48.0 || ^4.0.0" + "svelte": "^3.48.0 || >4.0.0" + } + }, + "node_modules/svelte-maplibre": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.7.3.tgz", + "integrity": "sha512-mF/wAHQKqrutC6NxnEBDWfszfcQiYusyyE5ulbRVuwWayC0ZTm9lkm376nmNfgruAJOe0QzPx4Mdxa7c2JlGLA==", + "dependencies": { + "d3-geo": "^3.1.0", + "just-compare": "^2.3.0", + "just-flush": "^2.3.0", + "maplibre-gl": "^3.5.0", + "pmtiles": "^2.10.0" + }, + "peerDependencies": { + "@deck.gl/core": "^8.8.0", + "@deck.gl/layers": "^8.8.0", + "@deck.gl/mapbox": "^8.8.0", + "svelte": "^3.54.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@deck.gl/core": { + "optional": true + }, + "@deck.gl/layers": { + "optional": true + }, + "@deck.gl/mapbox": { + "optional": true + } } }, "node_modules/svelte-preprocess": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.4.tgz", - "integrity": "sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.1.tgz", + "integrity": "sha512-p/Dp4hmrBW5mrCCq29lEMFpIJT2FZsRlouxEc5qpbOmXRbaFs7clLs8oKPwD3xCFyZfv1bIhvOzpQkhMEVQdMw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -11449,7 +11348,7 @@ "sass": "^1.26.8", "stylus": "^0.55.0", "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0", + "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { @@ -11485,26 +11384,10 @@ } } }, - "node_modules/svelte/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/svelte/node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dependencies": { - "@types/estree": "*" - } - }, "node_modules/svelte/node_modules/magic-string": { - "version": "0.30.4", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", - "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -11518,20 +11401,10 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, - "node_modules/tailwind-merge": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", - "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, "node_modules/tailwindcss": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", - "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", + "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -11539,10 +11412,10 @@ "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.12", + "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.18.2", + "jiti": "^1.19.1", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -11595,9 +11468,9 @@ } }, "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", "dev": true, "engines": { "node": ">= 14" @@ -11679,6 +11552,11 @@ "globrex": "^0.1.2" } }, + "node_modules/tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -11742,6 +11620,18 @@ "node": ">=12" } }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -11754,27 +11644,6 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -11809,9 +11678,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -11821,6 +11690,19 @@ "node": ">=14.17" } }, + "node_modules/typewise": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz", + "integrity": "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==", + "dependencies": { + "typewise-core": "^1.2.0" + } + }, + "node_modules/typewise-core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz", + "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==" + }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -11834,9 +11716,9 @@ } }, "node_modules/undici": { - "version": "5.25.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.25.3.tgz", - "integrity": "sha512-7lmhlz3K1+IKB6IUjkdzV2l0jKY8/0KguEMdEpzzXCug5pEGIp3DxUg0DEN65DrVoxHiRKpPORC/qzX+UglSkQ==", + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz", + "integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==", "dev": true, "dependencies": { "@fastify/busboy": "^2.0.0" @@ -11885,6 +11767,20 @@ "node": ">=4" } }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -11970,9 +11866,9 @@ "dev": true }, "node_modules/vite": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", - "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -12038,6 +11934,16 @@ } } }, + "node_modules/vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "dependencies": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -12348,8759 +12254,5 @@ "url": "https://github.com/sponsors/sindresorhus" } } - }, - "dependencies": { - "@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true - }, - "@adobe/css-tools": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz", - "integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==", - "dev": true - }, - "@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true - }, - "@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "requires": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - } - }, - "@babel/compat-data": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", - "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", - "dev": true - }, - "@babel/core": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.0.tgz", - "integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.0", - "@babel/types": "^7.23.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - } - }, - "@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", - "dev": true, - "requires": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", - "dev": true, - "requires": { - "@babel/types": "^7.22.15" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "semver": "^6.3.1" - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.1" - } - }, - "@babel/helper-define-polyfill-provider": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz", - "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - } - }, - "@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true - }, - "@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "requires": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", - "dev": true, - "requires": { - "@babel/types": "^7.23.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dev": true, - "requires": { - "@babel/types": "^7.22.15" - } - }, - "@babel/helper-module-transforms": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", - "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "dev": true - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" - } - }, - "@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" - } - }, - "@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", - "dev": true - }, - "@babel/helper-wrap-function": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", - "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.15", - "@babel/types": "^7.22.19" - } - }, - "@babel/helpers": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.1.tgz", - "integrity": "sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA==", - "dev": true, - "requires": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.0", - "@babel/types": "^7.23.0" - } - }, - "@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true - }, - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", - "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", - "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.15" - } - }, - "@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "requires": {} - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", - "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-async-generator-functions": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.15.tgz", - "integrity": "sha512-jBm1Es25Y+tVoTi5rfd5t1KLmL8ogLKpXszboWOTTtGFGz2RKnQe2yn7HbZ+kb/B8N0FVSGQo874NSlOU1T4+w==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", - "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-class-static-block": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz", - "integrity": "sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.11", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", - "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-split-export-declaration": "^7.22.6", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", - "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-dynamic-import": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz", - "integrity": "sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-export-namespace-from": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz", - "integrity": "sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", - "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-json-strings": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz", - "integrity": "sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz", - "integrity": "sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", - "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", - "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", - "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz", - "integrity": "sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - } - }, - "@babel/plugin-transform-numeric-separator": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz", - "integrity": "sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - } - }, - "@babel/plugin-transform-object-rest-spread": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz", - "integrity": "sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.15" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" - } - }, - "@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz", - "integrity": "sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - } - }, - "@babel/plugin-transform-optional-chaining": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", - "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", - "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-private-property-in-object": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz", - "integrity": "sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.11", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", - "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.2" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-typescript": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.15.tgz", - "integrity": "sha512-1uirS0TnijxvQLnlv5wQBwOX3E1wCFX7ITv+9pBV2wKEk4K+M5tqDaoNXnTH8tjEIYHLO98MwiTWO04Ggz4XuA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.22.5" - } - }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", - "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/preset-env": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz", - "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.22.20", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.15", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.15", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.11", - "@babel/plugin-transform-classes": "^7.22.15", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.15", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.11", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.11", - "@babel/plugin-transform-for-of": "^7.22.15", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.11", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.15", - "@babel/plugin-transform-modules-systemjs": "^7.22.11", - "@babel/plugin-transform-modules-umd": "^7.22.5", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-numeric-separator": "^7.22.11", - "@babel/plugin-transform-object-rest-spread": "^7.22.15", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.22.15", - "@babel/plugin-transform-parameters": "^7.22.15", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.11", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.10", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.10", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "@babel/types": "^7.22.19", - "babel-plugin-polyfill-corejs2": "^0.4.5", - "babel-plugin-polyfill-corejs3": "^0.8.3", - "babel-plugin-polyfill-regenerator": "^0.5.2", - "core-js-compat": "^3.31.0", - "semver": "^6.3.1" - } - }, - "@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - } - }, - "@babel/preset-typescript": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.0.tgz", - "integrity": "sha512-6P6VVa/NM/VlAYj5s2Aq/gdVg8FSENCg3wlZ6Qau9AcPaoF5LbN1nyGlR9DTRIw9PpxI94e+ReydsJHcjwAweg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/plugin-transform-typescript": "^7.22.15" - } - }, - "@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true - }, - "@babel/runtime": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", - "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.14.0" - } - }, - "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - } - }, - "@babel/traverse": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz", - "integrity": "sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "@cfcs/core": { - "version": "0.0.24", - "resolved": "https://registry.npmjs.org/@cfcs/core/-/core-0.0.24.tgz", - "integrity": "sha512-feB38qu+eDk0Pggh/yR7gjaNmvUYA2uCxHP3Pz2MLE4LZ/9jPdtu8bzCSI47yTEhWyZCF5Pk698hdz8IN2mTjA==", - "requires": { - "@egjs/component": "^3.0.4" - } - }, - "@egjs/component": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@egjs/component/-/component-3.0.4.tgz", - "integrity": "sha512-sXA7bGbIeLF2OAw/vpka66c6QBBUPcA4UUhR4WGJfnp2XWdiI8QrnJGJMr/UxpE/xnevX9tN3jvNPlW8WkHl3g==" - }, - "@egjs/imready": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@egjs/imready/-/imready-1.4.1.tgz", - "integrity": "sha512-JIOBs4lB7FYdsKi5uvz2j3SObX8eShtZjtqlOH41tm185aJOQZwiKBK8+V4MxzG4X6DqVhpdN8UcuVwBbElfsg==", - "requires": { - "@cfcs/core": "^0.0.24", - "@egjs/component": "^3.0.1" - } - }, - "@egjs/svelte-view360": { - "version": "4.0.0-beta.7", - "resolved": "https://registry.npmjs.org/@egjs/svelte-view360/-/svelte-view360-4.0.0-beta.7.tgz", - "integrity": "sha512-qFNbLNME8H7QU2lg8SCKUTPoBXVdBcM5m8zmlDRE72esCTguDzUq2szXD7L1JWcb2lYPTFl3HVp/sZlcQ/1HpQ==", - "requires": { - "@egjs/view360": "4.0.0-beta.7" - } - }, - "@egjs/view360": { - "version": "4.0.0-beta.7", - "resolved": "https://registry.npmjs.org/@egjs/view360/-/view360-4.0.0-beta.7.tgz", - "integrity": "sha512-prVTTxuQ1/k59NM7G0tm58k2vPHGoaExoFr5E7MoJaSGF56Otj4okQHAxxosXH87aQLN0feZMtBlsKz0b/7zEw==", - "requires": { - "@egjs/component": "^3.0.2", - "@egjs/imready": "^1.3.0", - "@types/webxr": "^0.5.1", - "gl-matrix": "^3.4.3" - } - }, - "@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "dev": true, - "optional": true - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", - "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - } - } - }, - "@eslint/js": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz", - "integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==", - "dev": true - }, - "@faker-js/faker": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", - "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", - "dev": true - }, - "@fastify/busboy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", - "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==", - "dev": true - }, - "@floating-ui/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", - "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", - "dev": true, - "requires": { - "@floating-ui/utils": "^0.1.3" - } - }, - "@floating-ui/dom": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", - "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", - "dev": true, - "requires": { - "@floating-ui/core": "^1.4.2", - "@floating-ui/utils": "^0.1.3" - } - }, - "@floating-ui/utils": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", - "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - } - }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "requires": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "requires": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - } - }, - "@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "requires": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - } - }, - "@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "requires": { - "jest-get-type": "^29.6.3" - } - }, - "@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - } - }, - "@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "requires": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - } - }, - "@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.0.tgz", - "integrity": "sha512-x58orMzEVfzPUKqlbLd1hXCnySCxKdDKa6Rjg97CwuLLRI4g3FHTdnExu1OqffVFay6zeMW+T6/DowFLndWnIw==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.27.8" - } - }, - "@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - } - }, - "@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "requires": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "requires": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - } - }, - "@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - }, - "@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", - "requires": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "@mdi/js": { - "version": "7.3.67", - "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.3.67.tgz", - "integrity": "sha512-MnRjknFqpTC6FifhGHjZ0+QYq2bAkZFQqIj8JA2AdPZbBxUvr8QSgB2yPAJ8/ob/XkR41xlg5majDR3c1JP1hw==" - }, - "@namnode/store": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@namnode/store/-/store-0.1.0.tgz", - "integrity": "sha512-4NGTldxKcmY0UuZ7OEkvCjs8ZEoeYB6M2UwMu74pdLiFMKxXbj9HdNk1Qn213bxX1O7bY5h+PLh5DZsTURZkYA==" - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@polka/url": { - "version": "1.0.0-next.23", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.23.tgz", - "integrity": "sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==", - "dev": true - }, - "@rollup/plugin-commonjs": { - "version": "25.0.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.4.tgz", - "integrity": "sha512-L92Vz9WUZXDnlQQl3EwbypJR4+DM2EbsO+/KOcEkP4Mc6Ct453EeDB2uH9lgRwj4w5yflgNpq9pHOiY8aoUXBQ==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "glob": "^8.0.3", - "is-reference": "1.2.1", - "magic-string": "^0.27.0" - } - }, - "@rollup/plugin-json": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.0.0.tgz", - "integrity": "sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^5.0.1" - } - }, - "@rollup/plugin-node-resolve": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.1.tgz", - "integrity": "sha512-nsbUg588+GDSu8/NS8T4UAshO6xeaOfINNuXeVHcKV02LJtoRaM1SiOacClw4kws1SFiNhdLGxlbMY9ga/zs/w==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-builtin-module": "^3.2.1", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - } - }, - "@rollup/pluginutils": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.4.tgz", - "integrity": "sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==", - "dev": true, - "requires": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - } - }, - "@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, - "@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "requires": { - "@sinonjs/commons": "^3.0.0" - } - }, - "@socket.io/component-emitter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" - }, - "@sveltejs/adapter-node": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-1.3.1.tgz", - "integrity": "sha512-A0VgRQDCDPzdLNoiAbcOxGw4zT1Mc+n1LwT1OmO350R7WxrEqdMUChPPOd1iMfIDWlP4ie6E2d/WQf5es2d4Zw==", - "dev": true, - "requires": { - "@rollup/plugin-commonjs": "^25.0.0", - "@rollup/plugin-json": "^6.0.0", - "@rollup/plugin-node-resolve": "^15.0.1", - "rollup": "^3.7.0" - } - }, - "@sveltejs/kit": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.25.1.tgz", - "integrity": "sha512-pD8XsvNJNgTNkFngNlM60my/X8dXWPKVzN5RghEQr0NjGZmuCjy49AfFu2cGbZjNf5pBcqd2RCNMW912P5fkhA==", - "dev": true, - "requires": { - "@sveltejs/vite-plugin-svelte": "^2.4.1", - "@types/cookie": "^0.5.1", - "cookie": "^0.5.0", - "devalue": "^4.3.1", - "esm-env": "^1.0.0", - "kleur": "^4.1.5", - "magic-string": "^0.30.0", - "mime": "^3.0.0", - "sade": "^1.8.1", - "set-cookie-parser": "^2.6.0", - "sirv": "^2.0.2", - "tiny-glob": "^0.2.9", - "undici": "~5.25.0" - }, - "dependencies": { - "magic-string": { - "version": "0.30.4", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", - "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - } - } - }, - "@sveltejs/vite-plugin-svelte": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.6.tgz", - "integrity": "sha512-zO79p0+DZnXPnF0ltIigWDx/ux7Ni+HRaFOw720Qeivc1azFUrJxTl0OryXVibYNx1hCboGia1NRV3x8RNv4cA==", - "dev": true, - "requires": { - "@sveltejs/vite-plugin-svelte-inspector": "^1.0.4", - "debug": "^4.3.4", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.3", - "svelte-hmr": "^0.15.3", - "vitefu": "^0.2.4" - }, - "dependencies": { - "magic-string": { - "version": "0.30.4", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", - "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - } - } - }, - "@sveltejs/vite-plugin-svelte-inspector": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz", - "integrity": "sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==", - "dev": true, - "requires": { - "debug": "^4.3.4" - } - }, - "@testing-library/dom": { - "version": "9.3.3", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", - "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "requires": { - "deep-equal": "^2.0.5" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@testing-library/jest-dom": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", - "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", - "dev": true, - "requires": { - "@adobe/css-tools": "^4.0.1", - "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", - "aria-query": "^5.0.0", - "chalk": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", - "lodash": "^4.17.15", - "redent": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@testing-library/svelte": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-4.0.3.tgz", - "integrity": "sha512-GldAnyGEOn5gMwME+hLVQrnfuKZFB+it5YOMnRBHX+nqeHMsSa18HeqkdvGqtqLpvn81xV7R7EYFb500ngUfXA==", - "dev": true, - "requires": { - "@testing-library/dom": "^9.3.1" - } - }, - "@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true - }, - "@types/aria-query": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.2.tgz", - "integrity": "sha512-PHKZuMN+K5qgKIWhBodXzQslTo5P+K/6LqeKXS6O/4liIDdZqaX5RXrCK++LAw+y/nptN48YmUMFiQHRSWYwtQ==", - "dev": true - }, - "@types/babel__core": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz", - "integrity": "sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==", - "dev": true, - "requires": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.5", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.5.tgz", - "integrity": "sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.2.tgz", - "integrity": "sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.2.tgz", - "integrity": "sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==", - "dev": true, - "requires": { - "@babel/types": "^7.20.7" - } - }, - "@types/cookie": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.2.tgz", - "integrity": "sha512-DBpRoJGKJZn7RY92dPrgoMew8xCWc2P71beqsjyhEI/Ds9mOyVmBwtekyfhpwFIVt1WrxTonFifiOZ62V8CnNA==", - "dev": true - }, - "@types/dom-to-image": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@types/dom-to-image/-/dom-to-image-2.6.5.tgz", - "integrity": "sha512-SMYQf4urvjHfSsaEMhIULyjfawUv2a92OfglcGF7dQdDHBjfnGPtWoxw6hmKMiwsdmowHn70mxLw+F5cA7Imyg==", - "dev": true - }, - "@types/estree": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz", - "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==" - }, - "@types/geojson": { - "version": "7946.0.11", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.11.tgz", - "integrity": "sha512-L7A0AINMXQpVwxHJ4jxD6/XjZ4NDufaRlUJHjNIFKYUFBH1SvOW+neaqb0VTRSLW5suSrSu19ObFEFnfNcr+qg==", - "dev": true - }, - "@types/graceful-fs": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.7.tgz", - "integrity": "sha512-MhzcwU8aUygZroVwL2jeYk6JisJrPl/oov/gsgGCue9mkgl9wjGbzReYQClxiUgFDnib9FuHqTndccKeZKxTRw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-gPQuzaPR5h/djlAv2apEG1HVOyj1IUs7GpfMZixU0/0KXT3pm64ylHuMUI1/Akh+sq/iikxg6Z2j+fcMDXaaTQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-kv43F9eb3Lhj+lr/Hn6OcLCs/sSM8bt+fIaP11rCYngfV6NVjzWXJ17owQtDQTL9tQ8WSLUrGsSJ6rJz0F1w1A==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, - "@types/jest": { - "version": "29.5.5", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.5.tgz", - "integrity": "sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==", - "dev": true, - "requires": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - } - } - }, - "@types/jsdom": { - "version": "20.0.1", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", - "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, - "@types/json-schema": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", - "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", - "dev": true - }, - "@types/justified-layout": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@types/justified-layout/-/justified-layout-4.1.1.tgz", - "integrity": "sha512-/ZJGVeDif6EHRzK3kUifyOekGJcBXD1s/eRYAYgkJHI4QAkohz62E0PSMbFrGpOdTulPWRgOAh1mFZbYw9a9iQ==", - "dev": true - }, - "@types/leaflet": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.6.tgz", - "integrity": "sha512-HakGTK5LBBWegNWsAmTlG55zN1zszYec7aG47/z6SzT90bW2vqjmbqk3YKAbrtveO+G7fSTKTYqVbIwAFnTrbg==", - "dev": true, - "requires": { - "@types/geojson": "*" - } - }, - "@types/leaflet.markercluster": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.2.tgz", - "integrity": "sha512-Yfi5R0Fb0xc/qotTuqJDAX65XKRt6DauzIdsMTcgrrhgf8DpBOrqPV/jw8/zUjY8FetRd6QdnefNdNJKmG/+zA==", - "dev": true, - "requires": { - "@types/leaflet": "*" - } - }, - "@types/lodash": { - "version": "4.14.199", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", - "integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==", - "dev": true - }, - "@types/lodash-es": { - "version": "4.17.9", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.9.tgz", - "integrity": "sha512-ZTcmhiI3NNU7dEvWLZJkzG6ao49zOIjEgIE0RgV7wbPxU0f2xT3VSAHw2gmst8swH6V0YkLRGp4qPlX/6I90MQ==", - "dev": true, - "requires": { - "@types/lodash": "*" - } - }, - "@types/luxon": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz", - "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==", - "dev": true - }, - "@types/node": { - "version": "20.8.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", - "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", - "dev": true - }, - "@types/pug": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.7.tgz", - "integrity": "sha512-I469DU0UXNC1aHepwirWhu9YKg5fkxohZD95Ey/5A7lovC+Siu+MCLffva87lnfThaOrw9Vb1DUN5t55oULAAw==", - "dev": true - }, - "@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true - }, - "@types/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", - "dev": true - }, - "@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, - "@types/testing-library__jest-dom": { - "version": "5.14.9", - "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", - "integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==", - "dev": true, - "requires": { - "@types/jest": "*" - } - }, - "@types/tough-cookie": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", - "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", - "dev": true - }, - "@types/webxr": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.5.tgz", - "integrity": "sha512-HVOsSRTQYx3zpVl0c0FBmmmcY/60BkQLzVnpE9M1aG4f2Z0aKlBWfj4XZ2zr++XNBfkQWYcwhGlmuu44RJPDqg==" - }, - "@types/yargs": { - "version": "17.0.26", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.26.tgz", - "integrity": "sha512-Y3vDy2X6zw/ZCumcwLpdhM5L7jmyGpmBCTYMHDLqT2IKVMYRRLdv6ZakA+wxhra6Z/3bwhNbNl9bDGXaFU+6rw==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ==", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - } - }, - "@zoom-image/core": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.24.0.tgz", - "integrity": "sha512-sN2bkyFelUtV9v07Nvhm+V86hVouEGfqsRz0Grqox943vMDn16izS2yRK3ZHuqeyMskvSDxCcC65p8B15ieX+w==", - "requires": { - "@namnode/store": "^0.1.0" - } - }, - "@zoom-image/svelte": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.1.16.tgz", - "integrity": "sha512-HxcsBaAj7XcDYdVF1ynMtbayNWB/csG0Iwtg35Q1IPeoKj+W/jVNYHeuzJgxeNnPGnN7ZyeAe/OX8c8BFW9XJg==", - "requires": { - "@zoom-image/core": "0.24.0" - } - }, - "abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "dev": true - }, - "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" - }, - "acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "dev": true, - "requires": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - } - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "requires": { - "dequal": "^2.0.3" - } - }, - "array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - } - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "autoprefixer": { - "version": "10.4.16", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", - "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", - "dev": true, - "requires": { - "browserslist": "^4.21.10", - "caniuse-lite": "^1.0.30001538", - "fraction.js": "^4.3.6", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - } - }, - "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true - }, - "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, - "axobject-query": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", - "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", - "requires": { - "dequal": "^2.0.3" - } - }, - "babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "requires": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - } - }, - "babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-plugin-polyfill-corejs2": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz", - "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.2", - "semver": "^6.3.1" - } - }, - "babel-plugin-polyfill-corejs3": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.4.tgz", - "integrity": "sha512-9l//BZZsPR+5XjyJMPtZSK4jv0BsTO1zDac2GC6ygx9WLGlcsnRd1Co0B2zT5fF5Ic6BZy+9m3HNZ3QcOeDKfg==", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.4.2", - "core-js-compat": "^3.32.2" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz", - "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.4.2" - } - }, - "babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.13" - } - }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001542", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001542.tgz", - "integrity": "sha512-UrtAXVcj1mvPBFQ4sKd38daP8dEcXXr5sQe6QNNinaPd0iA/cxg9/l3VrSdL73jgw5sKyuQ6jNgiKO12W3SsVA==", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", - "dev": true - }, - "cjs-module-lexer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", - "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", - "dev": true - }, - "cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - } - }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true - }, - "code-red": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", - "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1", - "acorn": "^8.10.0", - "estree-walker": "^3.0.3", - "periscopic": "^3.1.0" - }, - "dependencies": { - "estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "requires": { - "@types/estree": "^1.0.0" - } - } - } - }, - "collect-v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true - }, - "copy-image-clipboard": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/copy-image-clipboard/-/copy-image-clipboard-2.1.2.tgz", - "integrity": "sha512-3VCXVl2IpFfOyD8drv9DozcNlwmqBqxOlsgkEGyVAzadjlPk1go8YNZyy8QmTnwHPxSFpeCR9OdsStEdVK7qDA==" - }, - "core-js-compat": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz", - "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==", - "dev": true, - "requires": { - "browserslist": "^4.22.1" - } - }, - "create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "requires": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - } - }, - "css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, - "data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", - "dev": true, - "requires": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true - }, - "dedent": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", - "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", - "dev": true, - "requires": {} - }, - "deep-equal": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", - "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", - "dev": true, - "requires": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.1", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.0", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - } - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true - }, - "define-data-property": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", - "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", - "dev": true, - "requires": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - } - }, - "define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "requires": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" - }, - "detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true - }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - }, - "devalue": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz", - "integrity": "sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==", - "dev": true - }, - "didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true - }, - "dom-to-image": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/dom-to-image/-/dom-to-image-2.6.0.tgz", - "integrity": "sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==" - }, - "domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "dev": true, - "requires": { - "webidl-conversions": "^7.0.0" - } - }, - "electron-to-chromium": { - "version": "1.4.538", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.538.tgz", - "integrity": "sha512-1a2m63NEookb1beNFTGDihgF3CKL7ksZ7PSA0VloON5DpTEhnOVgaDes8xkrDhkXRxlcN8JymQDGnv+Nn+uvhg==", - "dev": true - }, - "emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "engine.io-client": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz", - "integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==", - "requires": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - }, - "dependencies": { - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "requires": {} - } - } - }, - "engine.io-parser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", - "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" - }, - "entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - } - }, - "es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true - }, - "esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "requires": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "source-map": "~0.6.1" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "eslint": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz", - "integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", - "@humanwhocodes/config-array": "^0.11.11", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - } - } - }, - "eslint-config-prettier": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", - "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", - "dev": true, - "requires": {} - }, - "eslint-plugin-svelte": { - "version": "2.33.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.33.2.tgz", - "integrity": "sha512-knWmauax+E/jvQ9CmuX5dAhQKP9P4eGQZxWa5RMutEJVCcy0wFmiUvOeDND2jR4vUkbDlX4khKjaceY7QzbkYw==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@jridgewell/sourcemap-codec": "^1.4.14", - "debug": "^4.3.1", - "esutils": "^2.0.3", - "known-css-properties": "^0.28.0", - "postcss": "^8.4.5", - "postcss-load-config": "^3.1.4", - "postcss-safe-parser": "^6.0.0", - "postcss-selector-parser": "^6.0.11", - "semver": "^7.5.3", - "svelte-eslint-parser": ">=0.33.0 <1.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true - }, - "esm-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", - "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==", - "dev": true - }, - "espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "requires": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true - }, - "expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "requires": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - } - }, - "factory.ts": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/factory.ts/-/factory.ts-1.4.1.tgz", - "integrity": "sha512-x5hrzGOZvQnw82ZK+fUo/p1nlbJGCi564FBx3jQWQix6xyEK8xvdCwjdgdmbaUiqfURWWfjgTJyBU5OSfs52tw==", - "dev": true, - "requires": { - "clone-deep": "^4.0.1", - "source-map-support": "^0.5.21" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", - "dev": true, - "requires": { - "flatted": "^3.2.7", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, - "follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" - }, - "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "requires": { - "is-callable": "^1.1.3" - } - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "fraction.js": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", - "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - } - }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, - "gl-matrix": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", - "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" - }, - "glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "globalyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", - "dev": true - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true - }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - } - }, - "harmony-reflect": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", - "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.1" - } - }, - "has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "dev": true, - "requires": { - "whatwg-encoding": "^2.0.0" - } - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "requires": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - } - }, - "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true - }, - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - }, - "identity-obj-proxy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", - "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", - "dev": true, - "requires": { - "harmony-reflect": "^1.4.6" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - } - } - }, - "import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "requires": { - "has-bigints": "^1.0.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", - "dev": true, - "requires": { - "builtin-modules": "^3.3.0" - } - }, - "is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true - }, - "is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "dev": true - }, - "is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, - "is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "requires": { - "@types/estree": "*" - } - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "dev": true - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, - "requires": { - "which-typed-array": "^1.1.11" - } - }, - "is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true - }, - "is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - } - }, - "istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - } - }, - "istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "requires": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - } - }, - "jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "requires": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - } - }, - "jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "requires": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "requires": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-environment-jsdom": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", - "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", - "dev": true, - "requires": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/jsdom": "^20.0.0", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0", - "jsdom": "^20.0.0" - } - }, - "jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "requires": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - } - }, - "jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true - }, - "jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - } - }, - "jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "requires": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - } - } - }, - "jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - } - }, - "jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "requires": {} - }, - "jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true - }, - "jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "requires": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - } - }, - "jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "requires": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "requires": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "requires": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "requires": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "requires": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jiti": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", - "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", - "dev": true, - "requires": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true - }, - "justified-layout": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/justified-layout/-/justified-layout-4.1.0.tgz", - "integrity": "sha512-M5FimNMXgiOYerVRGsXZ2YK9YNCaTtwtYp7Hb2308U1Q9TXXHx5G0p08mcVR5O53qf8bWY4NJcPBxE6zuayXSg==" - }, - "keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", - "dev": true, - "requires": { - "json-buffer": "3.0.1" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true - }, - "known-css-properties": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.28.0.tgz", - "integrity": "sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ==", - "dev": true - }, - "leaflet": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", - "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" - }, - "leaflet.markercluster": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", - "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", - "requires": {} - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true - }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true - }, - "locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "luxon": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", - "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==" - }, - "lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true - }, - "magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.13" - } - }, - "make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "requires": { - "semver": "^7.5.3" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "requires": { - "tmpl": "1.0.5" - } - }, - "mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "dev": true - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, - "min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" - }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "requires": { - "minimist": "^1.2.6" - } - }, - "mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true - }, - "mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "requires": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true - }, - "node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true - }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true - }, - "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true - }, - "object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "requires": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - }, - "dependencies": { - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - } - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dev": true, - "requires": { - "entities": "^4.4.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "requires": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" - }, - "dependencies": { - "estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "requires": { - "@types/estree": "^1.0.0" - } - }, - "is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "requires": { - "@types/estree": "*" - } - } - } - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true - }, - "pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, - "requires": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - } - }, - "postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "requires": { - "camelcase-css": "^2.0.1" - } - }, - "postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "requires": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - } - }, - "postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.11" - } - }, - "postcss-safe-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", - "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", - "dev": true, - "requires": {} - }, - "postcss-scss": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", - "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", - "dev": true, - "requires": {} - }, - "postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true - }, - "prettier-plugin-svelte": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-2.10.1.tgz", - "integrity": "sha512-Wlq7Z5v2ueCubWo0TZzKc9XHcm7TDxqcuzRuGd0gcENfzfT4JZ9yDlCbEgxWgiPmLHkBjfOtpAWkcT28MCDpUQ==", - "dev": true, - "requires": {} - }, - "pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "dependencies": { - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true - } - } - }, - "psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, - "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true - }, - "pure-rand": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", - "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", - "dev": true - }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, - "read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "requires": { - "pify": "^2.3.0" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "regenerate-unicode-properties": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", - "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", - "dev": true, - "requires": { - "regenerate": "^1.4.2" - } - }, - "regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true - }, - "regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.8.4" - } - }, - "regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" - } - }, - "regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "dev": true, - "requires": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - } - }, - "regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true - } - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, - "resolve": { - "version": "1.22.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", - "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", - "dev": true, - "requires": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", - "dev": true, - "requires": { - "fsevents": "~2.3.2" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "requires": { - "mri": "^1.1.0" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sander": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", - "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", - "dev": true, - "requires": { - "es6-promise": "^3.1.2", - "graceful-fs": "^4.1.3", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.2" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "requires": { - "xmlchars": "^2.2.0" - } - }, - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - }, - "set-cookie-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", - "dev": true - }, - "set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", - "dev": true, - "requires": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - } - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "sirv": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.3.tgz", - "integrity": "sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==", - "dev": true, - "requires": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", - "totalist": "^3.0.0" - } - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "socket.io-client": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", - "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", - "requires": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.5.2", - "socket.io-parser": "~4.2.4" - } - }, - "socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "requires": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - } - }, - "sorcery": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", - "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.14", - "buffer-crc32": "^0.2.5", - "minimist": "^1.2.0", - "sander": "^0.5.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "requires": { - "escape-string-regexp": "^2.0.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - } - } - }, - "stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, - "requires": { - "internal-slot": "^1.0.4" - } - }, - "string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "requires": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "sucrase": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", - "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "7.1.6", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "dependencies": { - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "svelte": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.1.tgz", - "integrity": "sha512-LpLqY2Jr7cRxkrTc796/AaaoMLF/1ax7cto8Ot76wrvKQhrPmZ0JgajiWPmg9mTSDqO16SSLiD17r9MsvAPTmw==", - "requires": { - "@ampproject/remapping": "^2.2.1", - "@jridgewell/sourcemap-codec": "^1.4.15", - "@jridgewell/trace-mapping": "^0.3.18", - "acorn": "^8.9.0", - "aria-query": "^5.3.0", - "axobject-query": "^3.2.1", - "code-red": "^1.0.3", - "css-tree": "^2.3.1", - "estree-walker": "^3.0.3", - "is-reference": "^3.0.1", - "locate-character": "^3.0.0", - "magic-string": "^0.30.0", - "periscopic": "^3.1.0" - }, - "dependencies": { - "estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "requires": { - "@types/estree": "^1.0.0" - } - }, - "is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "requires": { - "@types/estree": "*" - } - }, - "magic-string": { - "version": "0.30.4", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.4.tgz", - "integrity": "sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==", - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - } - } - }, - "svelte-check": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.5.2.tgz", - "integrity": "sha512-5a/YWbiH4c+AqAUP+0VneiV5bP8YOk9JL3jwvN+k2PEPLgpu85bjQc5eE67+eIZBBwUEJzmO3I92OqKcqbp3fw==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.17", - "chokidar": "^3.4.1", - "fast-glob": "^3.2.7", - "import-fresh": "^3.2.1", - "picocolors": "^1.0.0", - "sade": "^1.7.4", - "svelte-preprocess": "^5.0.4", - "typescript": "^5.0.3" - } - }, - "svelte-eslint-parser": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.0.tgz", - "integrity": "sha512-5awZ6Bs+Tb/zQwa41PSdcLynAVQTwW0HGyCBjtbAQ59taLZqDgQSMzRlDmapjZdDtzERm0oXDZNE0E+PKJ6ryg==", - "dev": true, - "requires": { - "eslint-scope": "^7.0.0", - "eslint-visitor-keys": "^3.0.0", - "espree": "^9.0.0", - "postcss": "^8.4.28", - "postcss-scss": "^4.0.7" - }, - "dependencies": { - "eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "svelte-hmr": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", - "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", - "dev": true, - "requires": {} - }, - "svelte-jester": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/svelte-jester/-/svelte-jester-2.3.2.tgz", - "integrity": "sha512-JtxSz4FWAaCRBXbPsh4LcDs4Ua7zdXgLC0TZvT1R56hRV0dymmNP+abw67DTPF7sQPyNxWsOKd0Sl7Q8SnP8kg==", - "dev": true, - "requires": {} - }, - "svelte-loading-spinners": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/svelte-loading-spinners/-/svelte-loading-spinners-0.3.4.tgz", - "integrity": "sha512-vKaW71QMCBcTNijAGc0mUl8k3DQ66iYmp6MB8BMGCXyWk82bTrcLy8FOnSm9fE+8q6TwzD6PLUoYFHt0II93Xw==" - }, - "svelte-local-storage-store": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.5.0.tgz", - "integrity": "sha512-SEDrpapeia6fUqta+r1NvSLlJYPkZ4pBcl15EYIOSPNzy6vhpoXu8cnzUDmZxsWl7fZGAHxrVH9UyZCbyO4W+g==", - "requires": {} - }, - "svelte-preprocess": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.4.tgz", - "integrity": "sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==", - "dev": true, - "requires": { - "@types/pug": "^2.0.6", - "detect-indent": "^6.1.0", - "magic-string": "^0.27.0", - "sorcery": "^0.11.0", - "strip-indent": "^3.0.0" - } - }, - "symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true - }, - "tailwind-merge": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", - "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==", - "dev": true - }, - "tailwindcss": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", - "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==", - "dev": true, - "requires": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.12", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.18.2", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "dependencies": { - "postcss-load-config": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", - "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", - "dev": true, - "requires": { - "lilconfig": "^2.0.5", - "yaml": "^2.1.1" - } - }, - "yaml": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", - "dev": true - } - } - }, - "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "requires": { - "any-promise": "^1.0.0" - } - }, - "thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "requires": { - "thenify": ">= 3.1.0 < 4" - } - }, - "thumbhash": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/thumbhash/-/thumbhash-0.1.1.tgz", - "integrity": "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg==" - }, - "tiny-glob": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", - "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", - "dev": true, - "requires": { - "globalyzer": "0.1.0", - "globrex": "^0.1.2" - } - }, - "tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true - }, - "tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", - "dev": true, - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - } - }, - "tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "dev": true, - "requires": { - "punycode": "^2.1.1" - } - }, - "ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true - }, - "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true - }, - "typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "dev": true - }, - "uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", - "optional": true - }, - "undici": { - "version": "5.25.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.25.3.tgz", - "integrity": "sha512-7lmhlz3K1+IKB6IUjkdzV2l0jKY8/0KguEMdEpzzXCug5pEGIp3DxUg0DEN65DrVoxHiRKpPORC/qzX+UglSkQ==", - "dev": true, - "requires": { - "@fastify/busboy": "^2.0.0" - } - }, - "unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true - }, - "universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true - }, - "update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "v8-to-istanbul": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", - "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0" - }, - "dependencies": { - "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - } - } - }, - "vite": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", - "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", - "dev": true, - "requires": { - "esbuild": "^0.18.10", - "fsevents": "~2.3.2", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - } - }, - "vitefu": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz", - "integrity": "sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==", - "dev": true, - "requires": {} - }, - "w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "dev": true, - "requires": { - "xml-name-validator": "^4.0.0" - } - }, - "walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "requires": { - "makeerror": "1.0.12" - } - }, - "webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true - }, - "whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, - "requires": { - "iconv-lite": "0.6.3" - } - }, - "whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "dev": true - }, - "whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "dev": true, - "requires": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, - "requires": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - } - }, - "which-typed-array": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - } - }, - "ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", - "dev": true, - "requires": {} - }, - "xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "dev": true - }, - "xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true - }, - "yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "requires": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - } - }, - "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - } } } diff --git a/web/package.json b/web/package.json index 7934c28b2..db7b2e8cf 100644 --- a/web/package.json +++ b/web/package.json @@ -22,38 +22,34 @@ "devDependencies": { "@babel/preset-env": "^7.20.2", "@babel/preset-typescript": "^7.22.5", - "@faker-js/faker": "^7.6.0", + "@faker-js/faker": "^8.0.0", "@floating-ui/dom": "^1.5.1", - "@sveltejs/adapter-node": "^1.2.0", + "@sveltejs/adapter-static": "^2.0.3", "@sveltejs/kit": "^1.20.4", - "@testing-library/jest-dom": "^5.16.5", + "@testing-library/jest-dom": "^6.0.0", "@testing-library/svelte": "^4.0.3", - "@types/cookie": "^0.5.1", "@types/dom-to-image": "^2.6.4", "@types/justified-layout": "^4.1.0", - "@types/leaflet": "^1.9.1", - "@types/leaflet.markercluster": "^1.5.1", "@types/lodash-es": "^4.17.6", "@types/luxon": "^3.2.0", - "@typescript-eslint/eslint-plugin": "^5.53.0", - "@typescript-eslint/parser": "^5.53.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", "autoprefixer": "^10.4.13", "babel-jest": "^29.4.3", "eslint": "^8.34.0", - "eslint-config-prettier": "^8.6.0", + "eslint-config-prettier": "^9.0.0", "eslint-plugin-svelte": "^2.30.0", "factory.ts": "^1.3.0", "identity-obj-proxy": "^3.0.0", "jest": "^29.4.3", "jest-environment-jsdom": "^29.4.3", "postcss": "^8.4.21", - "prettier": "^2.8.4", - "prettier-plugin-svelte": "^2.10.1", + "prettier": "^3.1.0", + "prettier-plugin-svelte": "^3.1.2", "svelte": "^4.0.5", "svelte-check": "^3.4.3", - "svelte-jester": "^2.3.2", + "svelte-jester": "^3.0.0", "svelte-preprocess": "^5.0.3", - "tailwind-merge": "^1.14.0", "tailwindcss": "^3.2.7", "tslib": "^2.5.0", "typescript": "^5.0.0", @@ -63,20 +59,20 @@ "dependencies": { "@egjs/svelte-view360": "^4.0.0-beta.7", "@mdi/js": "^7.3.67", - "@zoom-image/svelte": "^0.1.8", + "@zoom-image/svelte": "^0.2.0", "axios": "^0.27.2", "buffer": "^6.0.3", "copy-image-clipboard": "^2.1.2", "dom-to-image": "^2.6.0", "handlebars": "^4.7.7", "justified-layout": "^4.1.0", - "leaflet": "^1.9.4", - "leaflet.markercluster": "^1.5.3", "lodash-es": "^4.17.21", "luxon": "^3.2.1", + "maplibre-gl": "^3.6.0", "socket.io-client": "^4.6.1", "svelte-loading-spinners": "^0.3.4", - "svelte-local-storage-store": "^0.5.0", + "svelte-local-storage-store": "^0.6.0", + "svelte-maplibre": "^0.7.0", "thumbhash": "^0.1.1" } } diff --git a/web/src/api/api.ts b/web/src/api/api.ts index 3a3584ed4..fc6f49f02 100644 --- a/web/src/api/api.ts +++ b/web/src/api/api.ts @@ -26,7 +26,7 @@ import { BASE_PATH } from './open-api/base'; import { DUMMY_BASE_URL, toPathString } from './open-api/common'; import type { ApiParams } from './types'; -export class ImmichApi { +class ImmichApi { public activityApi: ActivityApi; public albumApi: AlbumApi; public libraryApi: LibraryApi; diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 9265d439f..ac5ea101e 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.84.0 + * The version of the OpenAPI document: 1.89.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -209,43 +209,6 @@ export interface AddUsersDto { */ 'sharedUserIds': Array; } -/** - * - * @export - * @interface AdminSignupResponseDto - */ -export interface AdminSignupResponseDto { - /** - * - * @type {string} - * @memberof AdminSignupResponseDto - */ - 'createdAt': string; - /** - * - * @type {string} - * @memberof AdminSignupResponseDto - */ - 'email': string; - /** - * - * @type {string} - * @memberof AdminSignupResponseDto - */ - 'firstName': string; - /** - * - * @type {string} - * @memberof AdminSignupResponseDto - */ - 'id': string; - /** - * - * @type {string} - * @memberof AdminSignupResponseDto - */ - 'lastName': string; -} /** * * @export @@ -331,6 +294,12 @@ export interface AlbumResponseDto { * @memberof AlbumResponseDto */ 'id': string; + /** + * + * @type {boolean} + * @memberof AlbumResponseDto + */ + 'isActivityEnabled': boolean; /** * * @type {string} @@ -478,6 +447,12 @@ export interface AssetBulkDeleteDto { * @interface AssetBulkUpdateDto */ export interface AssetBulkUpdateDto { + /** + * + * @type {string} + * @memberof AssetBulkUpdateDto + */ + 'dateTimeOriginal'?: string; /** * * @type {Array} @@ -496,6 +471,18 @@ export interface AssetBulkUpdateDto { * @memberof AssetBulkUpdateDto */ 'isFavorite'?: boolean; + /** + * + * @type {number} + * @memberof AssetBulkUpdateDto + */ + 'latitude'?: number; + /** + * + * @type {number} + * @memberof AssetBulkUpdateDto + */ + 'longitude'?: number; /** * * @type {boolean} @@ -701,6 +688,20 @@ export interface AssetJobsDto { } +/** + * + * @export + * @enum {string} + */ + +export const AssetOrder = { + Asc: 'asc', + Desc: 'desc' +} as const; + +export type AssetOrder = typeof AssetOrder[keyof typeof AssetOrder]; + + /** * * @export @@ -1181,22 +1182,6 @@ export interface CheckExistingAssetsResponseDto { */ 'existingIds': Array; } -/** - * - * @export - * @enum {string} - */ - -export const CitiesFile = { - Cities15000: 'cities15000', - Cities5000: 'cities5000', - Cities1000: 'cities1000', - Cities500: 'cities500' -} as const; - -export type CitiesFile = typeof CitiesFile[keyof typeof CitiesFile]; - - /** * * @export @@ -1372,24 +1357,18 @@ export interface CreateUserDto { * @memberof CreateUserDto */ 'externalPath'?: string | null; - /** - * - * @type {string} - * @memberof CreateUserDto - */ - 'firstName': string; - /** - * - * @type {string} - * @memberof CreateUserDto - */ - 'lastName': string; /** * * @type {boolean} * @memberof CreateUserDto */ 'memoriesEnabled'?: boolean; + /** + * + * @type {string} + * @memberof CreateUserDto + */ + 'name': string; /** * * @type {string} @@ -1796,97 +1775,6 @@ export interface FileReportItemDto { } -/** - * - * @export - * @interface ImportAssetDto - */ -export interface ImportAssetDto { - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'assetPath': string; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'deviceAssetId': string; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'deviceId': string; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'duration'?: string; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'fileCreatedAt': string; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'fileModifiedAt': string; - /** - * - * @type {boolean} - * @memberof ImportAssetDto - */ - 'isArchived'?: boolean; - /** - * - * @type {boolean} - * @memberof ImportAssetDto - */ - 'isExternal'?: boolean; - /** - * - * @type {boolean} - * @memberof ImportAssetDto - */ - 'isFavorite'?: boolean; - /** - * - * @type {boolean} - * @memberof ImportAssetDto - */ - 'isOffline'?: boolean; - /** - * - * @type {boolean} - * @memberof ImportAssetDto - */ - 'isReadOnly'?: boolean; - /** - * - * @type {boolean} - * @memberof ImportAssetDto - */ - 'isVisible'?: boolean; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'libraryId'?: string; - /** - * - * @type {string} - * @memberof ImportAssetDto - */ - 'sidecarPath'?: string; -} /** * * @export @@ -2168,12 +2056,6 @@ export interface LoginResponseDto { * @memberof LoginResponseDto */ 'accessToken': string; - /** - * - * @type {string} - * @memberof LoginResponseDto - */ - 'firstName': string; /** * * @type {boolean} @@ -2185,7 +2067,7 @@ export interface LoginResponseDto { * @type {string} * @memberof LoginResponseDto */ - 'lastName': string; + 'name': string; /** * * @type {string} @@ -2255,6 +2137,20 @@ export interface MapMarkerResponseDto { */ 'lon': number; } +/** + * + * @export + * @enum {string} + */ + +export const MapTheme = { + Light: 'light', + Dark: 'dark' +} as const; + +export type MapTheme = typeof MapTheme[keyof typeof MapTheme]; + + /** * * @export @@ -2378,6 +2274,105 @@ export interface OAuthConfigResponseDto { */ 'url'?: string; } +/** + * + * @export + * @interface PartnerResponseDto + */ +export interface PartnerResponseDto { + /** + * + * @type {UserAvatarColor} + * @memberof PartnerResponseDto + */ + 'avatarColor': UserAvatarColor; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'createdAt': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'deletedAt': string | null; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'email': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'externalPath': string | null; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'id': string; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'inTimeline'?: boolean; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'isAdmin': boolean; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'memoriesEnabled'?: boolean; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'name': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'oauthId': string; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'profileImagePath': string; + /** + * + * @type {boolean} + * @memberof PartnerResponseDto + */ + 'shouldChangePassword': boolean; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'storageLabel': string | null; + /** + * + * @type {string} + * @memberof PartnerResponseDto + */ + 'updatedAt': string; +} + + /** * * @export @@ -2587,6 +2582,20 @@ export interface QueueStatusDto { */ 'isPaused': boolean; } +/** + * + * @export + * @enum {string} + */ + +export const ReactionLevel = { + Album: 'album', + Asset: 'asset' +} as const; + +export type ReactionLevel = typeof ReactionLevel[keyof typeof ReactionLevel]; + + /** * * @export @@ -2696,19 +2705,6 @@ export interface SearchAlbumResponseDto { */ 'total': number; } -/** - * - * @export - * @interface SearchAssetDto - */ -export interface SearchAssetDto { - /** - * - * @type {string} - * @memberof SearchAssetDto - */ - 'searchTerm': string; -} /** * * @export @@ -2853,12 +2849,6 @@ export interface ServerConfigDto { * @memberof ServerConfigDto */ 'loginPageMessage': string; - /** - * - * @type {string} - * @memberof ServerConfigDto - */ - 'mapTileUrl': string; /** * * @type {string} @@ -3343,13 +3333,7 @@ export interface SignUpDto { * @type {string} * @memberof SignUpDto */ - 'firstName': string; - /** - * - * @type {string} - * @memberof SignUpDto - */ - 'lastName': string; + 'name': string; /** * * @type {string} @@ -3726,6 +3710,12 @@ export interface SystemConfigMachineLearningDto { * @interface SystemConfigMapDto */ export interface SystemConfigMapDto { + /** + * + * @type {string} + * @memberof SystemConfigMapDto + */ + 'darkStyle': string; /** * * @type {boolean} @@ -3737,7 +3727,7 @@ export interface SystemConfigMapDto { * @type {string} * @memberof SystemConfigMapDto */ - 'tileUrl': string; + 'lightStyle': string; } /** * @@ -3844,12 +3834,6 @@ export interface SystemConfigPasswordLoginDto { * @interface SystemConfigReverseGeocodingDto */ export interface SystemConfigReverseGeocodingDto { - /** - * - * @type {CitiesFile} - * @memberof SystemConfigReverseGeocodingDto - */ - 'citiesFileOverride': CitiesFile; /** * * @type {boolean} @@ -3857,8 +3841,6 @@ export interface SystemConfigReverseGeocodingDto { */ 'enabled': boolean; } - - /** * * @export @@ -4160,6 +4142,12 @@ export interface UpdateAlbumDto { * @memberof UpdateAlbumDto */ 'description'?: string; + /** + * + * @type {boolean} + * @memberof UpdateAlbumDto + */ + 'isActivityEnabled'?: boolean; } /** * @@ -4167,6 +4155,12 @@ export interface UpdateAlbumDto { * @interface UpdateAssetDto */ export interface UpdateAssetDto { + /** + * + * @type {string} + * @memberof UpdateAssetDto + */ + 'dateTimeOriginal'?: string; /** * * @type {string} @@ -4185,6 +4179,18 @@ export interface UpdateAssetDto { * @memberof UpdateAssetDto */ 'isFavorite'?: boolean; + /** + * + * @type {number} + * @memberof UpdateAssetDto + */ + 'latitude'?: number; + /** + * + * @type {number} + * @memberof UpdateAssetDto + */ + 'longitude'?: number; } /** * @@ -4217,6 +4223,19 @@ export interface UpdateLibraryDto { */ 'name'?: string; } +/** + * + * @export + * @interface UpdatePartnerDto + */ +export interface UpdatePartnerDto { + /** + * + * @type {boolean} + * @memberof UpdatePartnerDto + */ + 'inTimeline': boolean; +} /** * * @export @@ -4255,6 +4274,12 @@ export interface UpdateTagDto { * @interface UpdateUserDto */ export interface UpdateUserDto { + /** + * + * @type {UserAvatarColor} + * @memberof UpdateUserDto + */ + 'avatarColor'?: UserAvatarColor; /** * * @type {string} @@ -4267,12 +4292,6 @@ export interface UpdateUserDto { * @memberof UpdateUserDto */ 'externalPath'?: string; - /** - * - * @type {string} - * @memberof UpdateUserDto - */ - 'firstName'?: string; /** * * @type {string} @@ -4285,18 +4304,18 @@ export interface UpdateUserDto { * @memberof UpdateUserDto */ 'isAdmin'?: boolean; - /** - * - * @type {string} - * @memberof UpdateUserDto - */ - 'lastName'?: string; /** * * @type {boolean} * @memberof UpdateUserDto */ 'memoriesEnabled'?: boolean; + /** + * + * @type {string} + * @memberof UpdateUserDto + */ + 'name'?: string; /** * * @type {string} @@ -4316,6 +4335,8 @@ export interface UpdateUserDto { */ 'storageLabel'?: string; } + + /** * * @export @@ -4334,12 +4355,6 @@ export interface UsageByUserDto { * @memberof UsageByUserDto */ 'usage': number; - /** - * - * @type {string} - * @memberof UsageByUserDto - */ - 'userFirstName': string; /** * * @type {string} @@ -4351,7 +4366,7 @@ export interface UsageByUserDto { * @type {string} * @memberof UsageByUserDto */ - 'userLastName': string; + 'userName': string; /** * * @type {number} @@ -4359,24 +4374,46 @@ export interface UsageByUserDto { */ 'videos': number; } +/** + * + * @export + * @enum {string} + */ + +export const UserAvatarColor = { + Primary: 'primary', + Pink: 'pink', + Red: 'red', + Yellow: 'yellow', + Blue: 'blue', + Green: 'green', + Purple: 'purple', + Orange: 'orange', + Gray: 'gray', + Amber: 'amber' +} as const; + +export type UserAvatarColor = typeof UserAvatarColor[keyof typeof UserAvatarColor]; + + /** * * @export * @interface UserDto */ export interface UserDto { + /** + * + * @type {UserAvatarColor} + * @memberof UserDto + */ + 'avatarColor': UserAvatarColor; /** * * @type {string} * @memberof UserDto */ 'email': string; - /** - * - * @type {string} - * @memberof UserDto - */ - 'firstName': string; /** * * @type {string} @@ -4388,7 +4425,7 @@ export interface UserDto { * @type {string} * @memberof UserDto */ - 'lastName': string; + 'name': string; /** * * @type {string} @@ -4396,12 +4433,20 @@ export interface UserDto { */ 'profileImagePath': string; } + + /** * * @export * @interface UserResponseDto */ export interface UserResponseDto { + /** + * + * @type {UserAvatarColor} + * @memberof UserResponseDto + */ + 'avatarColor': UserAvatarColor; /** * * @type {string} @@ -4426,12 +4471,6 @@ export interface UserResponseDto { * @memberof UserResponseDto */ 'externalPath': string | null; - /** - * - * @type {string} - * @memberof UserResponseDto - */ - 'firstName': string; /** * * @type {string} @@ -4444,18 +4483,18 @@ export interface UserResponseDto { * @memberof UserResponseDto */ 'isAdmin': boolean; - /** - * - * @type {string} - * @memberof UserResponseDto - */ - 'lastName': string; /** * * @type {boolean} * @memberof UserResponseDto */ 'memoriesEnabled'?: boolean; + /** + * + * @type {string} + * @memberof UserResponseDto + */ + 'name': string; /** * * @type {string} @@ -4487,6 +4526,8 @@ export interface UserResponseDto { */ 'updatedAt': string; } + + /** * * @export @@ -5076,11 +5117,12 @@ export const ActivityApiAxiosParamCreator = function (configuration?: Configurat * @param {string} albumId * @param {string} [assetId] * @param {ReactionType} [type] + * @param {ReactionLevel} [level] * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getActivities: async (albumId: string, assetId?: string, type?: ReactionType, userId?: string, options: AxiosRequestConfig = {}): Promise => { + getActivities: async (albumId: string, assetId?: string, type?: ReactionType, level?: ReactionLevel, userId?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'albumId' is not null or undefined assertParamExists('getActivities', 'albumId', albumId) const localVarPath = `/activity`; @@ -5116,6 +5158,10 @@ export const ActivityApiAxiosParamCreator = function (configuration?: Configurat localVarQueryParameter['type'] = type; } + if (level !== undefined) { + localVarQueryParameter['level'] = level; + } + if (userId !== undefined) { localVarQueryParameter['userId'] = userId; } @@ -5216,12 +5262,13 @@ export const ActivityApiFp = function(configuration?: Configuration) { * @param {string} albumId * @param {string} [assetId] * @param {ReactionType} [type] + * @param {ReactionLevel} [level] * @param {string} [userId] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getActivities(albumId: string, assetId?: string, type?: ReactionType, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, userId, options); + async getActivities(albumId: string, assetId?: string, type?: ReactionType, level?: ReactionLevel, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, level, userId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -5270,7 +5317,7 @@ export const ActivityApiFactory = function (configuration?: Configuration, baseP * @throws {RequiredError} */ getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(axios, basePath)); + return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.level, requestParameters.userId, options).then((request) => request(axios, basePath)); }, /** * @@ -5339,6 +5386,13 @@ export interface ActivityApiGetActivitiesRequest { */ readonly type?: ReactionType + /** + * + * @type {ReactionLevel} + * @memberof ActivityApiGetActivities + */ + readonly level?: ReactionLevel + /** * * @type {string} @@ -5405,7 +5459,7 @@ export class ActivityApi extends BaseAPI { * @memberof ActivityApi */ public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) { - return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(this.axios, this.basePath)); + return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.level, requestParameters.userId, options).then((request) => request(this.axios, this.basePath)); } /** @@ -6766,6 +6820,48 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Get all asset of a device that are in the database, ID only. + * @param {string} deviceId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllUserAssetsByDeviceId: async (deviceId: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'deviceId' is not null or undefined + assertParamExists('getAllUserAssetsByDeviceId', 'deviceId', deviceId) + const localVarPath = `/asset/device/{deviceId}` + .replace(`{${"deviceId"}}`, encodeURIComponent(String(deviceId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -7258,11 +7354,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { + getTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'size' is not null or undefined assertParamExists('getTimeBucket', 'size', size) // verify required parameter 'timeBucket' is not null or undefined @@ -7320,6 +7417,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['withStacked'] = withStacked; } + if (withPartners !== undefined) { + localVarQueryParameter['withPartners'] = withPartners; + } + if (timeBucket !== undefined) { localVarQueryParameter['timeBucket'] = timeBucket; } @@ -7349,11 +7450,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { + getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'size' is not null or undefined assertParamExists('getTimeBuckets', 'size', size) const localVarPath = `/asset/time-buckets`; @@ -7409,6 +7511,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarQueryParameter['withStacked'] = withStacked; } + if (withPartners !== undefined) { + localVarQueryParameter['withPartners'] = withPartners; + } + if (key !== undefined) { localVarQueryParameter['key'] = key; } @@ -7425,9 +7531,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }; }, /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {string} deviceId * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ getUserAssetsByDeviceId: async (deviceId: string, options: AxiosRequestConfig = {}): Promise => { @@ -7466,50 +7574,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration options: localVarRequestOptions, }; }, - /** - * - * @param {ImportAssetDto} importAssetDto - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - importFile: async (importAssetDto: ImportAssetDto, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'importAssetDto' is not null or undefined - assertParamExists('importFile', 'importAssetDto', importAssetDto) - const localVarPath = `/asset/import`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication cookie required - - // authentication api_key required - await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(importAssetDto, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {BulkIdsDto} bulkIdsDto @@ -7638,14 +7702,51 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }, /** * - * @param {SearchAssetDto} searchAssetDto + * @param {string} [id] + * @param {string} [libraryId] + * @param {AssetTypeEnum} [type] + * @param {AssetOrder} [order] + * @param {string} [deviceAssetId] + * @param {string} [deviceId] + * @param {string} [checksum] + * @param {boolean} [isArchived] + * @param {boolean} [isEncoded] + * @param {boolean} [isExternal] + * @param {boolean} [isFavorite] + * @param {boolean} [isMotion] + * @param {boolean} [isOffline] + * @param {boolean} [isReadOnly] + * @param {boolean} [isVisible] + * @param {boolean} [withDeleted] + * @param {boolean} [withStacked] + * @param {boolean} [withExif] + * @param {boolean} [withPeople] + * @param {string} [createdBefore] + * @param {string} [createdAfter] + * @param {string} [updatedBefore] + * @param {string} [updatedAfter] + * @param {string} [trashedBefore] + * @param {string} [trashedAfter] + * @param {string} [takenBefore] + * @param {string} [takenAfter] + * @param {string} [originalFileName] + * @param {string} [originalPath] + * @param {string} [resizePath] + * @param {string} [webpPath] + * @param {string} [encodedVideoPath] + * @param {string} [city] + * @param {string} [state] + * @param {string} [country] + * @param {string} [make] + * @param {string} [model] + * @param {string} [lensModel] + * @param {number} [page] + * @param {number} [size] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - searchAsset: async (searchAssetDto: SearchAssetDto, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'searchAssetDto' is not null or undefined - assertParamExists('searchAsset', 'searchAssetDto', searchAssetDto) - const localVarPath = `/asset/search`; + searchAssets: async (id?: string, libraryId?: string, type?: AssetTypeEnum, order?: AssetOrder, deviceAssetId?: string, deviceId?: string, checksum?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, withDeleted?: boolean, withStacked?: boolean, withExif?: boolean, withPeople?: boolean, createdBefore?: string, createdAfter?: string, updatedBefore?: string, updatedAfter?: string, trashedBefore?: string, trashedAfter?: string, takenBefore?: string, takenAfter?: string, originalFileName?: string, originalPath?: string, resizePath?: string, webpPath?: string, encodedVideoPath?: string, city?: string, state?: string, country?: string, make?: string, model?: string, lensModel?: string, page?: number, size?: number, options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/assets`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -7653,7 +7754,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -7666,14 +7767,187 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) + if (id !== undefined) { + localVarQueryParameter['id'] = id; + } + + if (libraryId !== undefined) { + localVarQueryParameter['libraryId'] = libraryId; + } + + if (type !== undefined) { + localVarQueryParameter['type'] = type; + } + + if (order !== undefined) { + localVarQueryParameter['order'] = order; + } + + if (deviceAssetId !== undefined) { + localVarQueryParameter['deviceAssetId'] = deviceAssetId; + } + + if (deviceId !== undefined) { + localVarQueryParameter['deviceId'] = deviceId; + } + + if (checksum !== undefined) { + localVarQueryParameter['checksum'] = checksum; + } + + if (isArchived !== undefined) { + localVarQueryParameter['isArchived'] = isArchived; + } + + if (isEncoded !== undefined) { + localVarQueryParameter['isEncoded'] = isEncoded; + } + + if (isExternal !== undefined) { + localVarQueryParameter['isExternal'] = isExternal; + } + + if (isFavorite !== undefined) { + localVarQueryParameter['isFavorite'] = isFavorite; + } + + if (isMotion !== undefined) { + localVarQueryParameter['isMotion'] = isMotion; + } + + if (isOffline !== undefined) { + localVarQueryParameter['isOffline'] = isOffline; + } + + if (isReadOnly !== undefined) { + localVarQueryParameter['isReadOnly'] = isReadOnly; + } + + if (isVisible !== undefined) { + localVarQueryParameter['isVisible'] = isVisible; + } + + if (withDeleted !== undefined) { + localVarQueryParameter['withDeleted'] = withDeleted; + } + + if (withStacked !== undefined) { + localVarQueryParameter['withStacked'] = withStacked; + } + + if (withExif !== undefined) { + localVarQueryParameter['withExif'] = withExif; + } + + if (withPeople !== undefined) { + localVarQueryParameter['withPeople'] = withPeople; + } + + if (createdBefore !== undefined) { + localVarQueryParameter['createdBefore'] = (createdBefore as any instanceof Date) ? + (createdBefore as any).toISOString() : + createdBefore; + } + + if (createdAfter !== undefined) { + localVarQueryParameter['createdAfter'] = (createdAfter as any instanceof Date) ? + (createdAfter as any).toISOString() : + createdAfter; + } + + if (updatedBefore !== undefined) { + localVarQueryParameter['updatedBefore'] = (updatedBefore as any instanceof Date) ? + (updatedBefore as any).toISOString() : + updatedBefore; + } + + if (updatedAfter !== undefined) { + localVarQueryParameter['updatedAfter'] = (updatedAfter as any instanceof Date) ? + (updatedAfter as any).toISOString() : + updatedAfter; + } + + if (trashedBefore !== undefined) { + localVarQueryParameter['trashedBefore'] = (trashedBefore as any instanceof Date) ? + (trashedBefore as any).toISOString() : + trashedBefore; + } + + if (trashedAfter !== undefined) { + localVarQueryParameter['trashedAfter'] = (trashedAfter as any instanceof Date) ? + (trashedAfter as any).toISOString() : + trashedAfter; + } + + if (takenBefore !== undefined) { + localVarQueryParameter['takenBefore'] = (takenBefore as any instanceof Date) ? + (takenBefore as any).toISOString() : + takenBefore; + } + + if (takenAfter !== undefined) { + localVarQueryParameter['takenAfter'] = (takenAfter as any instanceof Date) ? + (takenAfter as any).toISOString() : + takenAfter; + } + + if (originalFileName !== undefined) { + localVarQueryParameter['originalFileName'] = originalFileName; + } + + if (originalPath !== undefined) { + localVarQueryParameter['originalPath'] = originalPath; + } + + if (resizePath !== undefined) { + localVarQueryParameter['resizePath'] = resizePath; + } + + if (webpPath !== undefined) { + localVarQueryParameter['webpPath'] = webpPath; + } + + if (encodedVideoPath !== undefined) { + localVarQueryParameter['encodedVideoPath'] = encodedVideoPath; + } + + if (city !== undefined) { + localVarQueryParameter['city'] = city; + } + + if (state !== undefined) { + localVarQueryParameter['state'] = state; + } + + if (country !== undefined) { + localVarQueryParameter['country'] = country; + } + + if (make !== undefined) { + localVarQueryParameter['make'] = make; + } + + if (model !== undefined) { + localVarQueryParameter['model'] = model; + } + + if (lensModel !== undefined) { + localVarQueryParameter['lensModel'] = lensModel; + } + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (size !== undefined) { + localVarQueryParameter['size'] = size; + } + - localVarHeaderParameter['Content-Type'] = 'application/json'; - setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(searchAssetDto, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -8093,6 +8367,16 @@ export const AssetApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(skip, take, userId, isFavorite, isArchived, updatedAfter, updatedBefore, ifNoneMatch, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * Get all asset of a device that are in the database, ID only. + * @param {string} deviceId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAllUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllUserAssetsByDeviceId(deviceId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * Get a single asset\'s information * @param {string} id @@ -8211,12 +8495,13 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options); + async getTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -8229,32 +8514,25 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {boolean} [isFavorite] * @param {boolean} [isTrashed] * @param {boolean} [withStacked] + * @param {boolean} [withPartners] * @param {string} [key] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options); - return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); - }, - /** - * Get all asset of a device that are in the database, ID only. - * @param {string} deviceId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async getUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getUserAssetsByDeviceId(deviceId, options); + async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, withPartners?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, withPartners, key, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * - * @param {ImportAssetDto} importAssetDto + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release + * @param {string} deviceId * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ - async importFile(importAssetDto: ImportAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.importFile(importAssetDto, options); + async getUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getUserAssetsByDeviceId(deviceId, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -8288,12 +8566,51 @@ export const AssetApiFp = function(configuration?: Configuration) { }, /** * - * @param {SearchAssetDto} searchAssetDto + * @param {string} [id] + * @param {string} [libraryId] + * @param {AssetTypeEnum} [type] + * @param {AssetOrder} [order] + * @param {string} [deviceAssetId] + * @param {string} [deviceId] + * @param {string} [checksum] + * @param {boolean} [isArchived] + * @param {boolean} [isEncoded] + * @param {boolean} [isExternal] + * @param {boolean} [isFavorite] + * @param {boolean} [isMotion] + * @param {boolean} [isOffline] + * @param {boolean} [isReadOnly] + * @param {boolean} [isVisible] + * @param {boolean} [withDeleted] + * @param {boolean} [withStacked] + * @param {boolean} [withExif] + * @param {boolean} [withPeople] + * @param {string} [createdBefore] + * @param {string} [createdAfter] + * @param {string} [updatedBefore] + * @param {string} [updatedAfter] + * @param {string} [trashedBefore] + * @param {string} [trashedAfter] + * @param {string} [takenBefore] + * @param {string} [takenAfter] + * @param {string} [originalFileName] + * @param {string} [originalPath] + * @param {string} [resizePath] + * @param {string} [webpPath] + * @param {string} [encodedVideoPath] + * @param {string} [city] + * @param {string} [state] + * @param {string} [country] + * @param {string} [make] + * @param {string} [model] + * @param {string} [lensModel] + * @param {number} [page] + * @param {number} [size] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async searchAsset(searchAssetDto: SearchAssetDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.searchAsset(searchAssetDto, options); + async searchAssets(id?: string, libraryId?: string, type?: AssetTypeEnum, order?: AssetOrder, deviceAssetId?: string, deviceId?: string, checksum?: string, isArchived?: boolean, isEncoded?: boolean, isExternal?: boolean, isFavorite?: boolean, isMotion?: boolean, isOffline?: boolean, isReadOnly?: boolean, isVisible?: boolean, withDeleted?: boolean, withStacked?: boolean, withExif?: boolean, withPeople?: boolean, createdBefore?: string, createdAfter?: string, updatedBefore?: string, updatedAfter?: string, trashedBefore?: string, trashedAfter?: string, takenBefore?: string, takenAfter?: string, originalFileName?: string, originalPath?: string, resizePath?: string, webpPath?: string, encodedVideoPath?: string, city?: string, state?: string, country?: string, make?: string, model?: string, lensModel?: string, page?: number, size?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.searchAssets(id, libraryId, type, order, deviceAssetId, deviceId, checksum, isArchived, isEncoded, isExternal, isFavorite, isMotion, isOffline, isReadOnly, isVisible, withDeleted, withStacked, withExif, withPeople, createdBefore, createdAfter, updatedBefore, updatedAfter, trashedBefore, trashedAfter, takenBefore, takenAfter, originalFileName, originalPath, resizePath, webpPath, encodedVideoPath, city, state, country, make, model, lensModel, page, size, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -8437,6 +8754,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath)); }, + /** + * Get all asset of a device that are in the database, ID only. + * @param {AssetApiGetAllUserAssetsByDeviceIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllUserAssetsByDeviceId(requestParameters: AssetApiGetAllUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(axios, basePath)); + }, /** * Get a single asset\'s information * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. @@ -8531,7 +8857,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath)); + return localVarFp.getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** * @@ -8540,26 +8866,19 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath)); + return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ getUserAssetsByDeviceId(requestParameters: AssetApiGetUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.getUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(axios, basePath)); }, - /** - * - * @param {AssetApiImportFileRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - importFile(requestParameters: AssetApiImportFileRequest, options?: AxiosRequestConfig): AxiosPromise { - return localVarFp.importFile(requestParameters.importAssetDto, options).then((request) => request(axios, basePath)); - }, /** * * @param {AssetApiRestoreAssetsRequest} requestParameters Request parameters. @@ -8588,12 +8907,12 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath }, /** * - * @param {AssetApiSearchAssetRequest} requestParameters Request parameters. + * @param {AssetApiSearchAssetsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} */ - searchAsset(requestParameters: AssetApiSearchAssetRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.searchAsset(requestParameters.searchAssetDto, options).then((request) => request(axios, basePath)); + searchAssets(requestParameters: AssetApiSearchAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.searchAssets(requestParameters.id, requestParameters.libraryId, requestParameters.type, requestParameters.order, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.checksum, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.withDeleted, requestParameters.withStacked, requestParameters.withExif, requestParameters.withPeople, requestParameters.createdBefore, requestParameters.createdAfter, requestParameters.updatedBefore, requestParameters.updatedAfter, requestParameters.trashedBefore, requestParameters.trashedAfter, requestParameters.takenBefore, requestParameters.takenAfter, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.resizePath, requestParameters.webpPath, requestParameters.encodedVideoPath, requestParameters.city, requestParameters.state, requestParameters.country, requestParameters.make, requestParameters.model, requestParameters.lensModel, requestParameters.page, requestParameters.size, options).then((request) => request(axios, basePath)); }, /** * @@ -8790,6 +9109,20 @@ export interface AssetApiGetAllAssetsRequest { readonly ifNoneMatch?: string } +/** + * Request parameters for getAllUserAssetsByDeviceId operation in AssetApi. + * @export + * @interface AssetApiGetAllUserAssetsByDeviceIdRequest + */ +export interface AssetApiGetAllUserAssetsByDeviceIdRequest { + /** + * + * @type {string} + * @memberof AssetApiGetAllUserAssetsByDeviceId + */ + readonly deviceId: string +} + /** * Request parameters for getAssetById operation in AssetApi. * @export @@ -9027,6 +9360,13 @@ export interface AssetApiGetTimeBucketRequest { */ readonly withStacked?: boolean + /** + * + * @type {boolean} + * @memberof AssetApiGetTimeBucket + */ + readonly withPartners?: boolean + /** * * @type {string} @@ -9097,6 +9437,13 @@ export interface AssetApiGetTimeBucketsRequest { */ readonly withStacked?: boolean + /** + * + * @type {boolean} + * @memberof AssetApiGetTimeBuckets + */ + readonly withPartners?: boolean + /** * * @type {string} @@ -9119,20 +9466,6 @@ export interface AssetApiGetUserAssetsByDeviceIdRequest { readonly deviceId: string } -/** - * Request parameters for importFile operation in AssetApi. - * @export - * @interface AssetApiImportFileRequest - */ -export interface AssetApiImportFileRequest { - /** - * - * @type {ImportAssetDto} - * @memberof AssetApiImportFile - */ - readonly importAssetDto: ImportAssetDto -} - /** * Request parameters for restoreAssets operation in AssetApi. * @export @@ -9162,17 +9495,290 @@ export interface AssetApiRunAssetJobsRequest { } /** - * Request parameters for searchAsset operation in AssetApi. + * Request parameters for searchAssets operation in AssetApi. * @export - * @interface AssetApiSearchAssetRequest + * @interface AssetApiSearchAssetsRequest */ -export interface AssetApiSearchAssetRequest { +export interface AssetApiSearchAssetsRequest { /** * - * @type {SearchAssetDto} - * @memberof AssetApiSearchAsset + * @type {string} + * @memberof AssetApiSearchAssets */ - readonly searchAssetDto: SearchAssetDto + readonly id?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly libraryId?: string + + /** + * + * @type {AssetTypeEnum} + * @memberof AssetApiSearchAssets + */ + readonly type?: AssetTypeEnum + + /** + * + * @type {AssetOrder} + * @memberof AssetApiSearchAssets + */ + readonly order?: AssetOrder + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly deviceAssetId?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly deviceId?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly checksum?: string + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isArchived?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isEncoded?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isExternal?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isFavorite?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isMotion?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isOffline?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isReadOnly?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly isVisible?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly withDeleted?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly withStacked?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly withExif?: boolean + + /** + * + * @type {boolean} + * @memberof AssetApiSearchAssets + */ + readonly withPeople?: boolean + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly createdBefore?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly createdAfter?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly updatedBefore?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly updatedAfter?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly trashedBefore?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly trashedAfter?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly takenBefore?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly takenAfter?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly originalFileName?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly originalPath?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly resizePath?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly webpPath?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly encodedVideoPath?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly city?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly state?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly country?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly make?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly model?: string + + /** + * + * @type {string} + * @memberof AssetApiSearchAssets + */ + readonly lensModel?: string + + /** + * + * @type {number} + * @memberof AssetApiSearchAssets + */ + readonly page?: number + + /** + * + * @type {number} + * @memberof AssetApiSearchAssets + */ + readonly size?: number } /** @@ -9461,6 +10067,17 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); } + /** + * Get all asset of a device that are in the database, ID only. + * @param {AssetApiGetAllUserAssetsByDeviceIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AssetApi + */ + public getAllUserAssetsByDeviceId(requestParameters: AssetApiGetAllUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath)); + } + /** * Get a single asset\'s information * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. @@ -9576,7 +10193,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getTimeBucket(requestParameters: AssetApiGetTimeBucketRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** @@ -9587,13 +10204,15 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); } /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} * @memberof AssetApi */ @@ -9601,17 +10220,6 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).getUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath)); } - /** - * - * @param {AssetApiImportFileRequest} requestParameters Request parameters. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof AssetApi - */ - public importFile(requestParameters: AssetApiImportFileRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).importFile(requestParameters.importAssetDto, options).then((request) => request(this.axios, this.basePath)); - } - /** * * @param {AssetApiRestoreAssetsRequest} requestParameters Request parameters. @@ -9646,13 +10254,13 @@ export class AssetApi extends BaseAPI { /** * - * @param {AssetApiSearchAssetRequest} requestParameters Request parameters. + * @param {AssetApiSearchAssetsRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AssetApi */ - public searchAsset(requestParameters: AssetApiSearchAssetRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).searchAsset(requestParameters.searchAssetDto, options).then((request) => request(this.axios, this.basePath)); + public searchAssets(requestParameters: AssetApiSearchAssetsRequest = {}, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).searchAssets(requestParameters.id, requestParameters.libraryId, requestParameters.type, requestParameters.order, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.checksum, requestParameters.isArchived, requestParameters.isEncoded, requestParameters.isExternal, requestParameters.isFavorite, requestParameters.isMotion, requestParameters.isOffline, requestParameters.isReadOnly, requestParameters.isVisible, requestParameters.withDeleted, requestParameters.withStacked, requestParameters.withExif, requestParameters.withPeople, requestParameters.createdBefore, requestParameters.createdAfter, requestParameters.updatedBefore, requestParameters.updatedAfter, requestParameters.trashedBefore, requestParameters.trashedAfter, requestParameters.takenBefore, requestParameters.takenAfter, requestParameters.originalFileName, requestParameters.originalPath, requestParameters.resizePath, requestParameters.webpPath, requestParameters.encodedVideoPath, requestParameters.city, requestParameters.state, requestParameters.country, requestParameters.make, requestParameters.model, requestParameters.lensModel, requestParameters.page, requestParameters.size, options).then((request) => request(this.axios, this.basePath)); } /** @@ -10497,7 +11105,7 @@ export const AuthenticationApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async signUpAdmin(signUpDto: SignUpDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async signUpAdmin(signUpDto: SignUpDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.signUpAdmin(signUpDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -10577,7 +11185,7 @@ export const AuthenticationApiFactory = function (configuration?: Configuration, * @param {*} [options] Override http request option. * @throws {RequiredError} */ - signUpAdmin(requestParameters: AuthenticationApiSignUpAdminRequest, options?: AxiosRequestConfig): AxiosPromise { + signUpAdmin(requestParameters: AuthenticationApiSignUpAdminRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.signUpAdmin(requestParameters.signUpDto, options).then((request) => request(axios, basePath)); }, /** @@ -12296,6 +12904,54 @@ export const PartnerApiAxiosParamCreator = function (configuration?: Configurati let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id + * @param {UpdatePartnerDto} updatePartnerDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartner: async (id: string, updatePartnerDto: UpdatePartnerDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('updatePartner', 'id', id) + // verify required parameter 'updatePartnerDto' is not null or undefined + assertParamExists('updatePartner', 'updatePartnerDto', updatePartnerDto) + const localVarPath = `/partner/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(updatePartnerDto, localVarRequestOptions, configuration) + return { url: toPathString(localVarUrlObj), options: localVarRequestOptions, @@ -12317,7 +12973,7 @@ export const PartnerApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async createPartner(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async createPartner(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.createPartner(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -12327,7 +12983,7 @@ export const PartnerApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getPartners(direction: 'shared-by' | 'shared-with', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + async getPartners(direction: 'shared-by' | 'shared-with', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { const localVarAxiosArgs = await localVarAxiosParamCreator.getPartners(direction, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -12341,6 +12997,17 @@ export const PartnerApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.removePartner(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} id + * @param {UpdatePartnerDto} updatePartnerDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updatePartner(id: string, updatePartnerDto: UpdatePartnerDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updatePartner(id, updatePartnerDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, } }; @@ -12357,7 +13024,7 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa * @param {*} [options] Override http request option. * @throws {RequiredError} */ - createPartner(requestParameters: PartnerApiCreatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { + createPartner(requestParameters: PartnerApiCreatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.createPartner(requestParameters.id, options).then((request) => request(axios, basePath)); }, /** @@ -12366,7 +13033,7 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getPartners(requestParameters: PartnerApiGetPartnersRequest, options?: AxiosRequestConfig): AxiosPromise> { + getPartners(requestParameters: PartnerApiGetPartnersRequest, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.getPartners(requestParameters.direction, options).then((request) => request(axios, basePath)); }, /** @@ -12378,6 +13045,15 @@ export const PartnerApiFactory = function (configuration?: Configuration, basePa removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.removePartner(requestParameters.id, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {PartnerApiUpdatePartnerRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updatePartner(requestParameters: PartnerApiUpdatePartnerRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.updatePartner(requestParameters.id, requestParameters.updatePartnerDto, options).then((request) => request(axios, basePath)); + }, }; }; @@ -12423,6 +13099,27 @@ export interface PartnerApiRemovePartnerRequest { readonly id: string } +/** + * Request parameters for updatePartner operation in PartnerApi. + * @export + * @interface PartnerApiUpdatePartnerRequest + */ +export interface PartnerApiUpdatePartnerRequest { + /** + * + * @type {string} + * @memberof PartnerApiUpdatePartner + */ + readonly id: string + + /** + * + * @type {UpdatePartnerDto} + * @memberof PartnerApiUpdatePartner + */ + readonly updatePartnerDto: UpdatePartnerDto +} + /** * PartnerApi - object-oriented interface * @export @@ -12462,6 +13159,17 @@ export class PartnerApi extends BaseAPI { public removePartner(requestParameters: PartnerApiRemovePartnerRequest, options?: AxiosRequestConfig) { return PartnerApiFp(this.configuration).removePartner(requestParameters.id, options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @param {PartnerApiUpdatePartnerRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof PartnerApi + */ + public updatePartner(requestParameters: PartnerApiUpdatePartnerRequest, options?: AxiosRequestConfig) { + return PartnerApiFp(this.configuration).updatePartner(requestParameters.id, requestParameters.updatePartnerDto, options).then((request) => request(this.axios, this.basePath)); + } } @@ -15088,6 +15796,51 @@ export const SystemConfigApiAxiosParamCreator = function (configuration?: Config + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {MapTheme} theme + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMapStyle: async (theme: MapTheme, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'theme' is not null or undefined + assertParamExists('getMapStyle', 'theme', theme) + const localVarPath = `/system-config/map/style.json`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (theme !== undefined) { + localVarQueryParameter['theme'] = theme; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -15207,6 +15960,16 @@ export const SystemConfigApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getConfigDefaults(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {MapTheme} theme + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getMapStyle(theme: MapTheme, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMapStyle(theme, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {*} [options] Override http request option. @@ -15252,6 +16015,15 @@ export const SystemConfigApiFactory = function (configuration?: Configuration, b getConfigDefaults(options?: AxiosRequestConfig): AxiosPromise { return localVarFp.getConfigDefaults(options).then((request) => request(axios, basePath)); }, + /** + * + * @param {SystemConfigApiGetMapStyleRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getMapStyle(requestParameters: SystemConfigApiGetMapStyleRequest, options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.getMapStyle(requestParameters.theme, options).then((request) => request(axios, basePath)); + }, /** * * @param {*} [options] Override http request option. @@ -15272,6 +16044,20 @@ export const SystemConfigApiFactory = function (configuration?: Configuration, b }; }; +/** + * Request parameters for getMapStyle operation in SystemConfigApi. + * @export + * @interface SystemConfigApiGetMapStyleRequest + */ +export interface SystemConfigApiGetMapStyleRequest { + /** + * + * @type {MapTheme} + * @memberof SystemConfigApiGetMapStyle + */ + readonly theme: MapTheme +} + /** * Request parameters for updateConfig operation in SystemConfigApi. * @export @@ -15313,6 +16099,17 @@ export class SystemConfigApi extends BaseAPI { return SystemConfigApiFp(this.configuration).getConfigDefaults(options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {SystemConfigApiGetMapStyleRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SystemConfigApi + */ + public getMapStyle(requestParameters: SystemConfigApiGetMapStyleRequest, options?: AxiosRequestConfig) { + return SystemConfigApiFp(this.configuration).getMapStyle(requestParameters.theme, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {*} [options] Override http request option. @@ -16184,6 +16981,44 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration) options: localVarRequestOptions, }; }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteProfileImage: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/user/profile-image`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {string} id @@ -16509,6 +17344,15 @@ export const UserApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteProfileImage(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteProfileImage(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {string} id @@ -16606,6 +17450,14 @@ export const UserApiFactory = function (configuration?: Configuration, basePath? createUser(requestParameters: UserApiCreateUserRequest, options?: AxiosRequestConfig): AxiosPromise { return localVarFp.createUser(requestParameters.createUserDto, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteProfileImage(options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.deleteProfileImage(options).then((request) => request(axios, basePath)); + }, /** * * @param {UserApiDeleteUserRequest} requestParameters Request parameters. @@ -16812,6 +17664,16 @@ export class UserApi extends BaseAPI { return UserApiFp(this.configuration).createUser(requestParameters.createUserDto, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof UserApi + */ + public deleteProfileImage(options?: AxiosRequestConfig) { + return UserApiFp(this.configuration).deleteProfileImage(options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {UserApiDeleteUserRequest} requestParameters Request parameters. diff --git a/web/src/api/open-api/base.ts b/web/src/api/open-api/base.ts index 9a534e7bd..f16265aff 100644 --- a/web/src/api/open-api/base.ts +++ b/web/src/api/open-api/base.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.84.0 + * The version of the OpenAPI document: 1.89.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/common.ts b/web/src/api/open-api/common.ts index 8997b2d52..c48b923a0 100644 --- a/web/src/api/open-api/common.ts +++ b/web/src/api/open-api/common.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.84.0 + * The version of the OpenAPI document: 1.89.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/configuration.ts b/web/src/api/open-api/configuration.ts index 8058881d1..030a53c76 100644 --- a/web/src/api/open-api/configuration.ts +++ b/web/src/api/open-api/configuration.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.84.0 + * The version of the OpenAPI document: 1.89.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/index.ts b/web/src/api/open-api/index.ts index d0651f28a..19d849d0c 100644 --- a/web/src/api/open-api/index.ts +++ b/web/src/api/open-api/index.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.84.0 + * The version of the OpenAPI document: 1.89.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/app.css b/web/src/app.css index c38700ba9..81b58580a 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -2,10 +2,39 @@ @tailwind components; @tailwind utilities; +@layer base { + :root { + /* light */ + --immich-primary: 66 80 175; + --immich-bg: 255 255 255; + --immich-fg: 0 0 0; + --immich-gray: 246 246 244; + --immich-error: 229 115 115; + --immich-success: 129 199 132; + --immich-warning: 255 183 77; + + /* dark */ + --immich-dark-primary: 172 203 250; + --immich-dark-bg: 0 0 0; + --immich-dark-fg: 229 231 235; + --immich-dark-gray: 33 33 33; + --immich-dark-error: 211 47 47; + --immich-dark-success: 56 142 60; + --immich-dark-warning: 245 124 0; + } +} @font-face { - font-family: 'Work Sans'; - src: url('$lib/assets/fonts/WorkSans-VariableFont_wght.ttf') format('truetype-variations'); + font-family: 'Overpass'; + src: url('$lib/assets/fonts/overpass/Overpass.ttf') format('truetype-variations'); font-weight: 1 999; + font-style: normal; +} + +@font-face { + font-family: 'Overpass Mono'; + src: url('$lib/assets/fonts/overpass/OverpassMono.ttf') format('truetype-variations'); + font-weight: 1 999; + font-style: monospace; } @font-face { @@ -14,7 +43,7 @@ } :root { - font-family: 'Work Sans', sans-serif; + font-family: 'Overpass', sans-serif; /* Used by layouts to ensure proper spacing between navbar and content */ --navbar-height: calc(theme(spacing.18) + 4px); } @@ -22,6 +51,7 @@ html { height: 100%; width: 100%; + font-size: 17px; } html::-webkit-scrollbar { diff --git a/web/src/app.d.ts b/web/src/app.d.ts index c29b346be..241a579fc 100644 --- a/web/src/app.d.ts +++ b/web/src/app.d.ts @@ -3,11 +3,6 @@ // See https://kit.svelte.dev/docs/types#app // for information about these interfaces declare namespace App { - interface Locals { - user?: import('@api').UserResponseDto; - api: import('@api').ImmichApi; - } - interface PageData { meta: { title: string; diff --git a/web/src/app.html b/web/src/app.html index 9da6868f7..5f31df333 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -1,4 +1,4 @@ - + @@ -13,6 +13,7 @@ document.documentElement.classList.remove('dark'); } + diff --git a/web/src/hooks.client.ts b/web/src/hooks.client.ts new file mode 100644 index 000000000..1e29371fa --- /dev/null +++ b/web/src/hooks.client.ts @@ -0,0 +1,40 @@ +import type { HandleClientError } from '@sveltejs/kit'; +import type { AxiosError, AxiosResponse } from 'axios'; + +const LOG_PREFIX = '[hooks.client.ts]'; +const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?'; + +const parseError = (error: unknown) => { + const httpError = error as AxiosError; + const request = httpError?.request as Request & { path: string }; + const response = httpError?.response as AxiosResponse<{ + message: string; + statusCode: number; + error: string; + }>; + + let code = response?.data?.statusCode || response?.status || httpError.code || '500'; + if (response) { + code += ` - ${response.data?.error || response.statusText}`; + } + + if (request && response) { + console.log({ + status: response.status, + url: `${request.method} ${request.path}`, + response: response.data || 'No data', + }); + } + + return { + message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE, + code, + stack: httpError?.stack, + }; +}; + +export const handleError: HandleClientError = ({ error }) => { + const result = parseError(error); + console.error(`${LOG_PREFIX}:handleError ${result.message}`); + return result; +}; diff --git a/web/src/hooks.server.ts b/web/src/hooks.server.ts deleted file mode 100644 index 809e3b0c3..000000000 --- a/web/src/hooks.server.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { env } from '$env/dynamic/public'; -import type { Handle, HandleServerError } from '@sveltejs/kit'; -import type { AxiosError, AxiosResponse } from 'axios'; -import { ImmichApi } from './api/api'; - -const LOG_PREFIX = '[hooks.server.ts]'; - -export const handle = (async ({ event, resolve }) => { - const basePath = env.PUBLIC_IMMICH_SERVER_URL || 'http://immich-server:3001'; - const accessToken = event.cookies.get('immich_access_token'); - const api = new ImmichApi({ basePath, accessToken }); - - // API instance that should be used for all server-side requests. - event.locals.api = api; - - if (accessToken) { - try { - const { data: user } = await api.userApi.getMyUserInfo(); - event.locals.user = user; - } catch (err) { - console.log(`${LOG_PREFIX} Unable to get my user`, parseError(err)); - - const apiError = err as AxiosError; - // Ignore 401 unauthorized errors and log all others. - if (apiError.response?.status && apiError.response?.status !== 401) { - console.error(`${LOG_PREFIX}:handle`, err); - } else if (!apiError.response?.status) { - console.error(`${LOG_PREFIX}:handle`, apiError?.message); - } - } - } - - const res = await resolve(event); - - // The link header can grow quite big and has caused issues with our nginx - // proxy returning a 502 Bad Gateway error. Therefore the header gets deleted. - res.headers.delete('Link'); - - return res; -}) satisfies Handle; - -const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?'; - -const parseError = (error: unknown) => { - const httpError = error as AxiosError; - const request = httpError?.request as Request & { path: string }; - const response = httpError?.response as AxiosResponse<{ - message: string; - statusCode: number; - error: string; - }>; - - let code = response?.data?.statusCode || response?.status || httpError.code || '500'; - if (response) { - code += ` - ${response.data?.error || response.statusText}`; - } - - if (request && response) { - console.log({ - status: response.status, - url: `${request.method} ${request.path}`, - response: response.data || 'No data', - }); - } - - return { - message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE, - code, - stack: httpError?.stack, - }; -}; - -export const handleError: HandleServerError = ({ error }) => { - const result = parseError(error); - console.error(`${LOG_PREFIX}:handleError ${result.message}`); - return result; -}; diff --git a/web/src/lib/assets/fonts/WorkSans-Italic-VariableFont_wght.ttf b/web/src/lib/assets/fonts/WorkSans-Italic-VariableFont_wght.ttf deleted file mode 100644 index 03e9671c1..000000000 Binary files a/web/src/lib/assets/fonts/WorkSans-Italic-VariableFont_wght.ttf and /dev/null differ diff --git a/web/src/lib/assets/fonts/WorkSans-VariableFont_wght.ttf b/web/src/lib/assets/fonts/WorkSans-VariableFont_wght.ttf deleted file mode 100644 index c8d05412f..000000000 Binary files a/web/src/lib/assets/fonts/WorkSans-VariableFont_wght.ttf and /dev/null differ diff --git a/web/src/lib/assets/fonts/overpass/Overpass-Italic.ttf b/web/src/lib/assets/fonts/overpass/Overpass-Italic.ttf new file mode 100644 index 000000000..281dd742b Binary files /dev/null and b/web/src/lib/assets/fonts/overpass/Overpass-Italic.ttf differ diff --git a/web/src/lib/assets/fonts/overpass/Overpass.ttf b/web/src/lib/assets/fonts/overpass/Overpass.ttf new file mode 100644 index 000000000..1cf730a5a Binary files /dev/null and b/web/src/lib/assets/fonts/overpass/Overpass.ttf differ diff --git a/web/src/lib/assets/fonts/overpass/OverpassMono.ttf b/web/src/lib/assets/fonts/overpass/OverpassMono.ttf new file mode 100644 index 000000000..71ef818b3 Binary files /dev/null and b/web/src/lib/assets/fonts/overpass/OverpassMono.ttf differ diff --git a/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte index 1b7261544..5bad6c940 100644 --- a/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte +++ b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte @@ -27,7 +27,7 @@

- {user.firstName} {user.lastName}'s account and assets will be permanently deleted after 7 days. + {user.name}'s account and assets will be permanently deleted after 7 days.

Are you sure you want to continue?

diff --git a/web/src/lib/components/admin-page/restore-dialoge.svelte b/web/src/lib/components/admin-page/restore-dialoge.svelte index 4e4517045..e96a4a54d 100644 --- a/web/src/lib/components/admin-page/restore-dialoge.svelte +++ b/web/src/lib/components/admin-page/restore-dialoge.svelte @@ -9,13 +9,16 @@ const restoreUser = async () => { const restoredUser = await api.userApi.restoreUser({ id: user.id }); - if (restoredUser.data.deletedAt == null) dispatch('user-restore-success'); - else dispatch('user-restore-fail'); + if (restoredUser.data.deletedAt == null) { + dispatch('user-restore-success'); + } else { + dispatch('user-restore-fail'); + } }; -

{user.firstName} {user.lastName}'s account will be restored.

+

{user.name}'s account will be restored.

diff --git a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte index 0dcddfb3b..aabb86d88 100644 --- a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte +++ b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte @@ -21,7 +21,8 @@ return '0'.repeat(zeroLength); }; - $: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, 0); + const TiB = 1024 ** 4; + $: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, stats.usage > TiB ? 2 : 0);
@@ -96,7 +97,7 @@ - {user.userFirstName} {user.userLastName} + {user.userName} {user.photos.toLocaleString($locale)} {user.videos.toLocaleString($locale)} {asByteUnitString(user.usage, $locale)} diff --git a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte index 9098bc33c..c945d5c9e 100644 --- a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte +++ b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte @@ -16,8 +16,18 @@ let savedConfig: SystemConfigJobDto; let defaultConfig: SystemConfigJobDto; - const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[]; - const jobNames = Object.values(JobName).filter((jobName) => !ignoredJobs.includes(jobName as JobName)); + const jobNames = [ + JobName.ThumbnailGeneration, + JobName.MetadataExtraction, + JobName.Library, + JobName.Sidecar, + JobName.ObjectTagging, + JobName.ClipEncoding, + JobName.RecognizeFaces, + JobName.VideoConversion, + JobName.StorageTemplateMigration, + JobName.Migration, + ]; async function getConfigs() { [savedConfig, defaultConfig] = await Promise.all([ diff --git a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte index e09157746..be6eb4135 100644 --- a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte +++ b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte @@ -160,11 +160,13 @@ +
@@ -123,24 +129,6 @@ subtitle="Enable reverse geocoding" bind:checked={config.reverseGeocoding.enabled} /> - -
- - diff --git a/web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte b/web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte index e1f4f89bf..3aaf75944 100644 --- a/web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte +++ b/web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte @@ -91,6 +91,7 @@ { value: 720, text: '720p' }, { value: 480, text: '480p' }, { value: 250, text: '250p' }, + { value: 200, text: '200p' }, ]} name="resolution" isEdited={thumbnailConfig.webpSize !== savedConfig.webpSize} @@ -105,6 +106,8 @@ options={[ { value: 2160, text: '4K' }, { value: 1440, text: '1440p' }, + { value: 1080, text: '1080p' }, + { value: 720, text: '720p' }, ]} name="resolution" isEdited={thumbnailConfig.jpegSize !== savedConfig.jpegSize} diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte index fdaf59395..040d5faa4 100644 --- a/web/src/lib/components/album-page/album-card.svelte +++ b/web/src/lib/components/album-page/album-card.svelte @@ -123,8 +123,7 @@

Owned

{:else}

- Shared by {albumOwner.firstName} - {albumOwner.lastName} + Shared by {albumOwner.name}

{/if} {/await} diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte new file mode 100644 index 000000000..5fecc6431 --- /dev/null +++ b/web/src/lib/components/album-page/album-options.svelte @@ -0,0 +1,76 @@ + + + dispatch('close')}> +
+
+
+
+

Options

+
+ dispatch('close')} /> +
+
+ +
+
+

SHARING

+
+ dispatch('toggleEnableActivity')} + /> +
+
+
+
PEOPLE
+
+ +
+
+ +
+
{user.name}
+
Owner
+
+ {#each album.sharedUsers as user (user.id)} +
+
+ +
+
{user.name}
+
+ {/each} +
+
+
+
+
+
+
diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 2333a55f0..609a03e25 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -20,6 +20,7 @@ import ThemeButton from '../shared-components/theme-button.svelte'; import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js'; + import UpdatePanel from '../shared-components/update-panel.svelte'; export let sharedLink: SharedLinkResponseDto; export let user: UserResponseDto | undefined = undefined; @@ -97,8 +98,12 @@
- {#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> + {#if $isMultiSelectState && user} + assetInteractionStore.clearMultiselect()} + > {#if sharedLink.allowDownload} @@ -139,7 +144,7 @@
- +

+
diff --git a/web/src/lib/components/album-page/share-info-modal.svelte b/web/src/lib/components/album-page/share-info-modal.svelte index 53656de5a..b92f20028 100644 --- a/web/src/lib/components/album-page/share-info-modal.svelte +++ b/web/src/lib/components/album-page/share-info-modal.svelte @@ -56,7 +56,7 @@ try { await api.albumApi.removeUserFromAlbum({ id: album.id, userId }); dispatch('remove', userId); - const message = userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.firstName}`; + const message = userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.name}`; notificationController.show({ type: NotificationType.Info, message }); } catch (e) { handleError(e, 'Unable to remove user'); @@ -77,8 +77,8 @@
- -

{album.owner.firstName} {album.owner.lastName}

+ +

{album.owner.name}

@@ -90,8 +90,8 @@ class="flex w-full place-items-center justify-between gap-4 p-5 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700" >
- -

{user.firstName} {user.lastName}

+ +

{user.name}

@@ -138,7 +138,7 @@ {#if selectedRemoveUser && selectedRemoveUser?.id !== currentUser?.id} (selectedRemoveUser = null)} diff --git a/web/src/lib/components/album-page/thumbnail-selection.svelte b/web/src/lib/components/album-page/thumbnail-selection.svelte index de984a46d..e58bf89d3 100644 --- a/web/src/lib/components/album-page/thumbnail-selection.svelte +++ b/web/src/lib/components/album-page/thumbnail-selection.svelte @@ -45,7 +45,7 @@
- {#each album.assets as asset} + {#each album.assets as asset (asset.id)} (selectedThumbnail = asset)} selected={isSelected(asset.id)} /> {/each}
diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte index 7a774a3f0..33b5494fc 100644 --- a/web/src/lib/components/album-page/user-selection-modal.svelte +++ b/web/src/lib/components/album-page/user-selection-modal.svelte @@ -71,8 +71,8 @@ on:click={() => handleUnselect(user)} class="flex place-items-center gap-1 rounded-full border border-gray-400 p-1 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700" > - -

{user.firstName} {user.lastName}

+ +

{user.name}

{/key} {/each} @@ -94,13 +94,12 @@ >✓ {:else} - + {/if}

- {user.firstName} - {user.lastName} + {user.name}

{user.email} diff --git a/web/src/lib/components/asset-viewer/activity-status.svelte b/web/src/lib/components/asset-viewer/activity-status.svelte index 23fdb7a9d..47e29ff97 100644 --- a/web/src/lib/components/asset-viewer/activity-status.svelte +++ b/web/src/lib/components/asset-viewer/activity-status.svelte @@ -7,6 +7,7 @@ export let isLiked: ActivityResponseDto | null; export let numberOfComments: number | undefined; export let isShowActivity: boolean | undefined; + export let disabled: boolean; const dispatch = createEventDispatcher(); @@ -14,7 +15,7 @@

-