diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 2c6d889d7b..6a9a01d41d 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -12,6 +12,10 @@ on: default: 'development' type: string secrets: + PUSH_O_MATIC_APP_CLIENT_ID: + required: true + PUSH_O_MATIC_APP_KEY: + required: true KEY_JKS: required: true ALIAS: @@ -59,7 +63,7 @@ jobs: - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 + uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -106,7 +110,7 @@ jobs: working-directory: ./mobile run: printf "%s" $KEY_JKS | base64 -d > android/key.jks - - uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0 + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: 'zulu' java-version: '17' @@ -221,6 +225,13 @@ jobs: github_token: ${{ steps.token.outputs.token }} working_directory: ./mobile + - name: Setup Ruby + uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 + with: + ruby-version: '3.3' + bundler-cache: true + working-directory: ./mobile/ios + - name: Install dependencies run: mise //mobile:install:ci @@ -231,18 +242,6 @@ jobs: working-directory: ./mobile run: flutter build ios --config-only --no-codesign - - name: Setup Ruby - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0 - with: - ruby-version: '3.3' - bundler-cache: true - working-directory: ./mobile/ios - - - name: Install CocoaPods dependencies - working-directory: ./mobile/ios - run: | - pod install - - name: Create API Key env: API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index a38f131a71..522444db97 100644 --- a/.github/workflows/check-openapi.yml +++ b/.github/workflows/check-openapi.yml @@ -25,7 +25,7 @@ jobs: persist-credentials: false - name: Check for breaking API changes - uses: oasdiff/oasdiff-action/breaking@0ab8ad204b00d25acc5ae87106281433e288d0c1 # v0.1.10 + uses: oasdiff/oasdiff-action/breaking@b7c3adeb54330db1903d27c61db520e5661ad55b # v0.1.11 with: base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json revision: open-api/immich-openapi-specs.json diff --git a/.github/workflows/close-duplicates.yml b/.github/workflows/close-duplicates.yml index cf932dac8d..a1ec4e80c8 100644 --- a/.github/workflows/close-duplicates.yml +++ b/.github/workflows/close-duplicates.yml @@ -39,7 +39,7 @@ jobs: needs: [get_body, should_run] if: ${{ needs.should_run.outputs.should_run == 'true' }} container: - image: ghcr.io/immich-app/mdq:main@sha256:d603f5618c1fc9f2b4d506ffed54ba6bcfec83d56b893b5fa8b3f899a402fa0a + image: ghcr.io/immich-app/mdq:main@sha256:74b93dfbf13f8ee73eb35d8d5195ff667f93f9bc55df5ae453dd43a461c821fa outputs: checked: ${{ steps.get_checkbox.outputs.checked }} steps: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7e14227a51..522de204ea 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -71,7 +71,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 + uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -84,6 +84,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 657d61d647..90ba3f40d6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -14,6 +14,10 @@ concurrency: permissions: {} +env: + GHCR_NAMESPACE: ghcr.io/${{ github.repository_owner }} + DOCKERHUB_NAMESPACE: docker.io/altran1502 + jobs: pre-job: runs-on: ubuntu-latest @@ -31,7 +35,7 @@ jobs: - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 + uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -44,8 +48,6 @@ jobs: - 'machine-learning/**' force-filters: | - '.github/workflows/docker.yml' - - '.github/workflows/multi-runner-build.yml' - - '.github/actions/image-build' force-events: 'workflow_dispatch,release' retag_ml: @@ -58,7 +60,30 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn'] + include: &ml-variants + - image: immich-machine-learning + device: cpu + - image: immich-machine-learning + device: cuda + suffixes: '-cuda' + platforms: linux/amd64 + - image: immich-machine-learning + device: openvino + suffixes: '-openvino' + platforms: linux/amd64 + - image: immich-machine-learning + device: armnn + suffixes: '-armnn' + platforms: linux/arm64 + - image: immich-machine-learning + device: rknn + suffixes: '-rknn' + platforms: linux/arm64 + - image: immich-machine-learning + device: rocm + suffixes: '-rocm' + platforms: linux/amd64 + runner-mapping: '{"linux/amd64": "pokedex-large"}' steps: - name: Login to GitHub Container Registry uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 @@ -70,10 +95,10 @@ jobs: - name: Re-tag image env: REGISTRY_NAME: 'ghcr.io' - REPOSITORY: ${{ github.repository_owner }}/immich-machine-learning - TAG_OLD: main${{ matrix.suffix }} - TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} - TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }} + REPOSITORY: ${{ github.repository_owner }}/${{ matrix.image }} + TAG_OLD: main${{ matrix.suffixes }} + TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffixes }} + TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffixes }} run: | docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" @@ -86,9 +111,6 @@ jobs: packages: write if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == false && !github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest - strategy: - matrix: - suffix: [''] steps: - name: Login to GitHub Container Registry uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 @@ -101,9 +123,9 @@ jobs: env: REGISTRY_NAME: 'ghcr.io' REPOSITORY: ${{ github.repository_owner }}/immich-server - TAG_OLD: main${{ matrix.suffix }} - TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }} - TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }} + TAG_OLD: main + TAG_PR: ${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }} + TAG_COMMIT: commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }} run: | docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}" @@ -115,40 +137,27 @@ jobs: strategy: fail-fast: false matrix: - include: - - device: cpu - - device: cuda - suffixes: '-cuda' - platforms: linux/amd64 - - device: openvino - suffixes: '-openvino' - platforms: linux/amd64 - - device: armnn - suffixes: '-armnn' - platforms: linux/arm64 - - device: rknn - suffixes: '-rknn' - platforms: linux/arm64 - - device: rocm - suffixes: '-rocm' - platforms: linux/amd64 - runner-mapping: '{"linux/amd64": "pokedex-large"}' - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@50dc3a14f0606ecd8fc28d78d3a3c655115ab695 # multi-runner-build-workflow-v3.1.0 + include: *ml-variants + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@4d420cf8e5ea499f99b2c2849ff9a219d5f7b178 # multi-runner-build-workflow-v4.0.0 permissions: contents: read actions: read packages: write - secrets: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} with: - image: immich-machine-learning + image: ${{ matrix.image }} context: machine-learning dockerfile: machine-learning/Dockerfile platforms: ${{ matrix.platforms }} runner-mapping: ${{ matrix.runner-mapping }} suffixes: ${{ matrix.suffixes }} - dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }} + tags: &tag-rules | + type=ref,event=branch + type=ref,event=pr + type=sha,format=long,prefix=commit- + type=semver,pattern={{version}},prefix=v + type=semver,pattern={{major}},prefix=v + type=match,pattern=v(\d+),group=1,prefix=v,suffix=-rc,enable=${{ github.event.release.prerelease == true }} + type=raw,value=release,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }} build-args: | DEVICE=${{ matrix.device }} @@ -156,25 +165,72 @@ jobs: name: Build and Push Server needs: pre-job if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} - uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@50dc3a14f0606ecd8fc28d78d3a3c655115ab695 # multi-runner-build-workflow-v3.1.0 + uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@4d420cf8e5ea499f99b2c2849ff9a219d5f7b178 # multi-runner-build-workflow-v4.0.0 permissions: contents: read actions: read packages: write - secrets: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} with: image: immich-server context: . dockerfile: server/Dockerfile - dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }} + tags: *tag-rules build-args: | DEVICE=cpu + mirror: + name: Mirror to Docker Hub + needs: [machine-learning, server] + if: ${{ github.event_name == 'release' && github.event.release.prerelease == false }} + permissions: + packages: read + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Slightly hacky; we reuse the ml matrix but tack on immich-server too + image: [immich-server] + include: *ml-variants + env: + IMAGE: ${{ matrix.image }} + SUFFIX: ${{ matrix.suffixes }} + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Generate docker image tags + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + flavor: | + latest=false + images: ${{ env.GHCR_NAMESPACE }}/${{ env.IMAGE }} + tags: *tag-rules + + - name: Mirror tags to Docker Hub + env: + TAGS: ${{ steps.meta.outputs.tags }} + run: | + for tag in ${TAGS}; do + version="${tag##*:}" + docker buildx imagetools create \ + -t "${DOCKERHUB_NAMESPACE}/${IMAGE}:${version}${SUFFIX}" \ + "${GHCR_NAMESPACE}/${IMAGE}:${version}${SUFFIX}" + done + success-check-server: name: Docker Build & Push Server Success - needs: [server, retag_server] + needs: [server, retag_server, mirror] permissions: {} runs-on: ubuntu-latest if: always() @@ -185,7 +241,7 @@ jobs: success-check-ml: name: Docker Build & Push ML Success - needs: [machine-learning, retag_ml] + needs: [machine-learning, retag_ml, mirror] permissions: {} runs-on: ubuntu-latest if: always() diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 6d7bd497d7..e0c5de3fab 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -29,7 +29,7 @@ jobs: - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 + uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0 with: github-token: ${{ steps.token.outputs.token }} filters: | diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 1bf9a61e8c..abc84c7cdb 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -17,10 +17,6 @@ on: - prepatch - prerelease - release - mobileBump: - description: 'Bump mobile build number' - required: false - type: boolean skipTranslations: description: 'Skip translations' required: false @@ -79,8 +75,7 @@ jobs: - name: Bump version env: RELEASE_TYPE: ${{ inputs.releaseType }} - MOBILE_BUMP: ${{ inputs.mobileBump }} - run: mise //:release --type "${RELEASE_TYPE}" --mobile "${MOBILE_BUMP}" + run: mise //:release --type "${RELEASE_TYPE}" - id: output run: | @@ -107,6 +102,8 @@ jobs: contents: read pull-requests: write secrets: + PUSH_O_MATIC_APP_CLIENT_ID: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} + PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }} KEY_JKS: ${{ secrets.KEY_JKS }} ALIAS: ${{ secrets.ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} @@ -152,19 +149,23 @@ jobs: github-token: ${{ steps.generate-token.outputs.token }} - name: Create draft release - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 - with: - draft: true - prerelease: ${{ needs.bump_version.outputs.rc }} - tag_name: ${{ needs.bump_version.outputs.version }} - token: ${{ steps.generate-token.outputs.token }} - generate_release_notes: true - body_path: misc/release/notes.tmpl - files: | - docker/docker-compose.yml - docker/docker-compose.rootless.yml - docker/example.env - docker/hwaccel.ml.yml - docker/hwaccel.transcoding.yml - docker/prometheus.yml + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + REPO: ${{ github.repository }} + TAG: ${{ needs.bump_version.outputs.version }} + PRERELEASE: ${{ needs.bump_version.outputs.rc }} + run: | + gh release create "${TAG}" \ + --repo "${REPO}" \ + --draft \ + --verify-tag \ + --prerelease="${PRERELEASE}" \ + --notes-file misc/release/notes.tmpl \ + --generate-notes \ + docker/docker-compose.yml \ + docker/docker-compose.rootless.yml \ + docker/example.env \ + docker/hwaccel.ml.yml \ + docker/hwaccel.transcoding.yml \ + docker/prometheus.yml \ *.apk diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 3004f0323f..0a3c61668c 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -28,7 +28,7 @@ jobs: - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 + uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0 with: github-token: ${{ steps.token.outputs.token }} filters: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index acf3a8d7f1..cb3448e870 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 + uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0 with: github-token: ${{ steps.token.outputs.token }} filters: | @@ -419,7 +419,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - name: Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -498,7 +498,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Setup pnpm - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - name: Setup Node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index 9447f4dea0..94a81f8f5d 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -32,7 +32,7 @@ jobs: - name: Check what should run id: check - uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4 + uses: immich-app/devtools/actions/pre-job@03514eceb4a27cd714976a866b350df5ce175720 # pre-job-action-v2.1.0 with: github-token: ${{ steps.token.outputs.token }} filters: | diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 779065e327..b717de02f9 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -97,7 +97,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:12.4.6-ubuntu@sha256:124ddf024f95e61b562a02112b47bc64de75973370cf440b1356d31cd8178844 + image: grafana/grafana:12.4.7-ubuntu@sha256:d327f509400334fbea74a282d287f20c3187222741ec99e76f253cac16c90129 volumes: - grafana-data:/var/lib/grafana diff --git a/docs/mise.toml b/docs/mise.toml index c4fe5f1b68..da5d00940e 100644 --- a/docs/mise.toml +++ b/docs/mise.toml @@ -28,4 +28,4 @@ run = "prettier --write ." run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}" [tools] -wrangler = "4.114.0" +wrangler = "4.119.0" diff --git a/docs/src/components/version-switcher.tsx b/docs/src/components/version-switcher.tsx index 739d7bd001..967c85c780 100644 --- a/docs/src/components/version-switcher.tsx +++ b/docs/src/components/version-switcher.tsx @@ -2,6 +2,8 @@ import { useWindowSize } from '@docusaurus/theme-common'; import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem'; import React, { useEffect, useState } from 'react'; +const ARCHIVED_VERSIONS_URL = 'https://version.immich.cloud/v1/docs/versions'; + export default function VersionSwitcher(): JSX.Element { const [versions, setVersions] = useState([]); const [activeLabel, setLabel] = useState('Versions'); @@ -11,12 +13,7 @@ export default function VersionSwitcher(): JSX.Element { useEffect(() => { async function getVersions() { try { - let baseUrl = 'https://docs.immich.app'; - if (window.location.origin === 'http://localhost:3005') { - baseUrl = window.location.origin; - } - - const response = await fetch(`${baseUrl}/archived-versions.json`); + const response = await fetch(ARCHIVED_VERSIONS_URL); const archiveVersions = await response.json(); diff --git a/docs/static/_redirects b/docs/static/_redirects index 633025ac17..e218124e59 100644 --- a/docs/static/_redirects +++ b/docs/static/_redirects @@ -1,3 +1,4 @@ +/archived-versions.json https://version.immich.cloud/v1/docs/versions 307 / /overview/quick-start 307 /mobile-app-beta-program /features/mobile-app 307 /contribution-guidelines /overview/support-the-project#contributing 307 diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json deleted file mode 100644 index dbeed76b73..0000000000 --- a/docs/static/archived-versions.json +++ /dev/null @@ -1,265 +0,0 @@ -[ - { - "label": "v3.1.0", - "url": "https://docs.v3.1.0.archive.immich.app" - }, - { - "label": "v3.0.3", - "url": "https://docs.v3.0.3.archive.immich.app" - }, - { - "label": "v2.7.5", - "url": "https://docs.v2.7.5.archive.immich.app" - }, - { - "label": "v2.6.3", - "url": "https://docs.v2.6.3.archive.immich.app" - }, - { - "label": "v2.5.6", - "url": "https://docs.v2.5.6.archive.immich.app" - }, - { - "label": "v2.4.1", - "url": "https://docs.v2.4.1.archive.immich.app" - }, - { - "label": "v2.3.1", - "url": "https://docs.v2.3.1.archive.immich.app" - }, - { - "label": "v2.2.3", - "url": "https://docs.v2.2.3.archive.immich.app" - }, - { - "label": "v2.1.0", - "url": "https://docs.v2.1.0.archive.immich.app" - }, - { - "label": "v2.0.1", - "url": "https://docs.v2.0.1.archive.immich.app" - }, - { - "label": "v1.144.1", - "url": "https://docs.v1.144.1.archive.immich.app" - }, - { - "label": "v1.143.1", - "url": "https://docs.v1.143.1.archive.immich.app" - }, - { - "label": "v1.142.1", - "url": "https://v1.142.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.141.1", - "url": "https://v1.141.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.140.1", - "url": "https://v1.140.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.139.4", - "url": "https://v1.139.4.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.138.1", - "url": "https://v1.138.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.137.3", - "url": "https://v1.137.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.136.0", - "url": "https://v1.136.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.135.3", - "url": "https://v1.135.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.134.0", - "url": "https://v1.134.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.133.1", - "url": "https://v1.133.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.132.3", - "url": "https://v1.132.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.131.3", - "url": "https://v1.131.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.130.3", - "url": "https://v1.130.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.129.0", - "url": "https://v1.129.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.128.0", - "url": "https://v1.128.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.127.0", - "url": "https://v1.127.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.126.1", - "url": "https://v1.126.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.125.7", - "url": "https://v1.125.7.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.124.2", - "url": "https://v1.124.2.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.123.0", - "url": "https://v1.123.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.122.3", - "url": "https://v1.122.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.121.0", - "url": "https://v1.121.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.120.2", - "url": "https://v1.120.2.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.119.1", - "url": "https://v1.119.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.118.2", - "url": "https://v1.118.2.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.117.0", - "url": "https://v1.117.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.116.2", - "url": "https://v1.116.2.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.115.0", - "url": "https://v1.115.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.114.0", - "url": "https://v1.114.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.113.1", - "url": "https://v1.113.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.112.1", - "url": "https://v1.112.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.111.0", - "url": "https://v1.111.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.110.0", - "url": "https://v1.110.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.109.2", - "url": "https://v1.109.2.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.108.0", - "url": "https://v1.108.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.107.2", - "url": "https://v1.107.2.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.106.4", - "url": "https://v1.106.4.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.105.1", - "url": "https://v1.105.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.104.0", - "url": "https://v1.104.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.103.1", - "url": "https://v1.103.1.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.102.3", - "url": "https://v1.102.3.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.101.0", - "url": "https://v1.101.0.archive.immich.app", - "rootPath": "/docs" - }, - { - "label": "v1.100.0", - "url": "https://v1.100.0.archive.immich.app", - "rootPath": "/docs" - } -] diff --git a/e2e/eslint.config.mjs b/e2e/eslint.config.mjs index d5da8f8445..3d69d1f503 100644 --- a/e2e/eslint.config.mjs +++ b/e2e/eslint.config.mjs @@ -2,13 +2,8 @@ import js from '@eslint/js'; import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; import eslintPluginUnicorn from 'eslint-plugin-unicorn'; import globals from 'globals'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import typescriptEslint from 'typescript-eslint'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - export default typescriptEslint.config([ eslintPluginUnicorn.configs.recommended, eslintPluginPrettierRecommended, @@ -29,7 +24,7 @@ export default typescriptEslint.config([ parserOptions: { project: 'tsconfig.json', - tsconfigRootDir: __dirname, + tsconfigRootDir: import.meta.dirname, }, }, diff --git a/e2e/src/specs/server/api/partner.e2e-spec.ts b/e2e/src/specs/server/api/partner.e2e-spec.ts index 9047a97055..0eb7d6db3d 100644 --- a/e2e/src/specs/server/api/partner.e2e-spec.ts +++ b/e2e/src/specs/server/api/partner.e2e-spec.ts @@ -1,6 +1,5 @@ import { LoginResponseDto, createPartner } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; -import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -29,13 +28,6 @@ describe('/partners', () => { }); describe('GET /partners', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/partners'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get all partners shared by user', async () => { const { status, body } = await request(app) .get('/partners') @@ -58,13 +50,6 @@ describe('/partners', () => { }); describe('POST /partners/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post(`/partners/${user3.userId}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should share with new partner', async () => { const { status, body } = await request(app) .post(`/partners/${user3.userId}`) @@ -85,13 +70,6 @@ describe('/partners', () => { }); describe('PUT /partners/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/partners/${user2.userId}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should update partner', async () => { const { status, body } = await request(app) .put(`/partners/${user2.userId}`) @@ -104,13 +82,6 @@ describe('/partners', () => { }); describe('DELETE /partners/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/partners/${user3.userId}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should delete partner', async () => { const { status } = await request(app) .delete(`/partners/${user3.userId}`) diff --git a/e2e/src/specs/server/api/server.e2e-spec.ts b/e2e/src/specs/server/api/server.e2e-spec.ts index 53857a31fa..f0eed82f7a 100644 --- a/e2e/src/specs/server/api/server.e2e-spec.ts +++ b/e2e/src/specs/server/api/server.e2e-spec.ts @@ -22,12 +22,6 @@ describe('/server', () => { }); describe('GET /server/about', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/server/about'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should return about information', async () => { const { status, body } = await request(app) .get('/server/about') @@ -56,12 +50,6 @@ describe('/server', () => { }); describe('GET /server/storage', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/server/storage'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should return the disk information', async () => { const { status, body } = await request(app) .get('/server/storage') @@ -149,12 +137,6 @@ describe('/server', () => { }); describe('GET /server/statistics', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/server/statistics'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should only work for admins', async () => { const { status, body } = await request(app) .get('/server/statistics') @@ -213,12 +195,6 @@ describe('/server', () => { }); describe('GET /server/license', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/server/license'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should only work for admins', async () => { const { status, body } = await request(app) .get('/server/license') @@ -241,12 +217,6 @@ describe('/server', () => { }); describe('DELETE /server/license', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete('/server/license'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should only work for admins', async () => { const { status, body } = await request(app) .delete('/server/license') @@ -266,12 +236,6 @@ describe('/server', () => { }); describe('PUT /server/license', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put('/server/license'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should only work for admins', async () => { const { status, body } = await request(app) .put('/server/license') diff --git a/e2e/src/specs/server/api/session.e2e-spec.ts b/e2e/src/specs/server/api/session.e2e-spec.ts index 0b632f78ba..1766b732cd 100644 --- a/e2e/src/specs/server/api/session.e2e-spec.ts +++ b/e2e/src/specs/server/api/session.e2e-spec.ts @@ -15,12 +15,6 @@ describe('/sessions', () => { }); describe('GET /sessions', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/sessions'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get a list of authorized devices', async () => { const { status, body } = await request(app).get('/sessions').set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); @@ -29,12 +23,6 @@ describe('/sessions', () => { }); describe('DELETE /sessions', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/sessions`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should logout all devices (except the current one)', async () => { for (let i = 0; i < 5; i++) { await login({ loginCredentialDto: loginDto.admin }); diff --git a/e2e/src/specs/server/api/shared-link.e2e-spec.ts b/e2e/src/specs/server/api/shared-link.e2e-spec.ts index 8cdf2dc03c..b9f695b48e 100644 --- a/e2e/src/specs/server/api/shared-link.e2e-spec.ts +++ b/e2e/src/specs/server/api/shared-link.e2e-spec.ts @@ -137,13 +137,6 @@ describe('/shared-links', () => { }); describe('GET /shared-links', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/shared-links'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get all shared links created by user', async () => { const { status, body } = await request(app) .get('/shared-links') @@ -200,12 +193,6 @@ describe('/shared-links', () => { }); describe('GET /shared-links/me', () => { - it('should not require admin authentication', async () => { - const { status } = await request(app).get('/shared-links/me').set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(403); - }); - it('should get data for correct shared link', async () => { const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key }); @@ -293,13 +280,6 @@ describe('/shared-links', () => { }); describe('GET /shared-links/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/shared-links/${linkWithAlbum.id}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should get shared link by id', async () => { const { status, body } = await request(app) .get(`/shared-links/${linkWithAlbum.id}`) @@ -326,26 +306,6 @@ describe('/shared-links', () => { }); describe('POST /shared-links', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .post('/shared-links') - .send({ type: SharedLinkType.Album, albumId: uuidDto.notFound }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a type and the correspondent asset/album id', async () => { - const { status, body } = await request(app) - .post('/shared-links') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(400); - expect(body).toEqual( - errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]), - ); - }); - it('should require an asset/album id', async () => { const { status, body } = await request(app) .post('/shared-links') @@ -383,15 +343,6 @@ describe('/shared-links', () => { }); describe('PATCH /shared-links/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .patch(`/shared-links/${linkWithAlbum.id}`) - .send({ description: 'foo' }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should fail if invalid link', async () => { const { status, body } = await request(app) .patch(`/shared-links/${uuidDto.notFound}`) @@ -474,13 +425,6 @@ describe('/shared-links', () => { }); describe('DELETE /shared-links/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/shared-links/${linkWithAlbum.id}`); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should fail if invalid link', async () => { const { status, body } = await request(app) .delete(`/shared-links/${uuidDto.notFound}`) diff --git a/e2e/src/specs/server/api/stack.e2e-spec.ts b/e2e/src/specs/server/api/stack.e2e-spec.ts index 76bf514dc8..3b8f642334 100644 --- a/e2e/src/specs/server/api/stack.e2e-spec.ts +++ b/e2e/src/specs/server/api/stack.e2e-spec.ts @@ -1,5 +1,5 @@ import { AssetMediaResponseDto, LoginResponseDto, searchStacks } from '@immich/sdk'; -import { createUserDto, uuidDto } from 'src/fixtures'; +import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; @@ -25,42 +25,6 @@ describe('/stacks', () => { }); describe('POST /stacks', () => { - it('should require authentication', async () => { - const { status, body } = await request(app) - .post('/stacks') - .send({ assetIds: [asset.id] }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require at least two assets', async () => { - const { status, body } = await request(app) - .post('/stacks') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ assetIds: [asset.id] }); - - expect(status).toBe(400); - expect(body).toEqual( - errorDto.validationError([{ path: ['assetIds'], message: 'Too small: expected array to have >=2 items' }]), - ); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .post('/stacks') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ assetIds: [uuidDto.invalid, uuidDto.invalid] }); - - expect(status).toBe(400); - expect(body).toEqual( - errorDto.validationError([ - { path: ['assetIds', 0], message: 'Invalid UUID' }, - { path: ['assetIds', 1], message: 'Invalid UUID' }, - ]), - ); - }); - it('should require access', async () => { const user2Asset = await utils.createAsset(user2.accessToken); const { status, body } = await request(app) diff --git a/e2e/src/specs/server/api/system-metadata.e2e-spec.ts b/e2e/src/specs/server/api/system-metadata.e2e-spec.ts index bd17bf2524..114ed2f995 100644 --- a/e2e/src/specs/server/api/system-metadata.e2e-spec.ts +++ b/e2e/src/specs/server/api/system-metadata.e2e-spec.ts @@ -16,12 +16,6 @@ describe('/server-info', () => { }); describe('POST /system-metadata/admin-onboarding', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/system-metadata/admin-onboarding').send({ isOnboarded: true }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should only work for admins', async () => { const { status, body } = await request(app) .post('/system-metadata/admin-onboarding') @@ -47,12 +41,6 @@ describe('/server-info', () => { }); describe('GET /system-metadata/reverse-geocoding-state', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/system-metadata/reverse-geocoding-state'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should only work for admins', async () => { const { status, body } = await request(app) .get('/system-metadata/reverse-geocoding-state') diff --git a/e2e/src/specs/server/api/tag.e2e-spec.ts b/e2e/src/specs/server/api/tag.e2e-spec.ts index d303a1e98d..98d66f6350 100644 --- a/e2e/src/specs/server/api/tag.e2e-spec.ts +++ b/e2e/src/specs/server/api/tag.e2e-spec.ts @@ -41,12 +41,6 @@ describe('/tags', () => { }); describe('POST /tags', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post('/tags').send({ name: 'TagA' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization (api key)', async () => { const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' }); @@ -117,12 +111,6 @@ describe('/tags', () => { }); describe('GET /tags', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get('/tags'); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization (api key)', async () => { const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); const { status, body } = await request(app).get('/tags').set('x-api-key', secret); @@ -169,12 +157,6 @@ describe('/tags', () => { }); describe('PUT /tags', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/tags`).send({ name: 'TagA/TagB' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization (api key)', async () => { const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); const { status, body } = await request(app).put('/tags').set('x-api-key', secret).send({ name: 'TagA' }); @@ -213,12 +195,6 @@ describe('/tags', () => { }); describe('PUT /tags/assets', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/tags/assets`).send({ tagIds: [], assetIds: [] }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization (api key)', async () => { const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); const { status, body } = await request(app) @@ -279,12 +255,6 @@ describe('/tags', () => { }); describe('GET /tags/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/tags/${uuidDto.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const tag = await create(user.accessToken, { name: 'TagA' }); const { status, body } = await request(app) @@ -304,14 +274,6 @@ describe('/tags', () => { expect(body).toEqual(errorDto.missingPermission('tag.read')); }); - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .get(`/tags/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); - }); - it('should get tag details', async () => { const tag = await create(user.accessToken, { name: 'TagA' }); const { status, body } = await request(app) @@ -349,13 +311,6 @@ describe('/tags', () => { }); describe('PUT /tags/:id', () => { - it('should require authentication', async () => { - const tag = await create(user.accessToken, { name: 'TagA' }); - const { status, body } = await request(app).put(`/tags/${tag.id}`).send({ color: '#000000' }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const tag = await create(admin.accessToken, { name: 'tagA' }); const { status, body } = await request(app) @@ -399,12 +354,6 @@ describe('/tags', () => { }); describe('DELETE /tags/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/tags/${uuidDto.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const tag = await create(user.accessToken, { name: 'TagA' }); const { status, body } = await request(app) @@ -422,14 +371,6 @@ describe('/tags', () => { expect(body).toEqual(errorDto.missingPermission('tag.delete')); }); - it('should require a valid uuid', async () => { - const { status, body } = await request(app) - .delete(`/tags/${uuidDto.invalid}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); - }); - it('should delete a tag', async () => { const tag = await create(user.accessToken, { name: 'TagA' }); const { status } = await request(app) @@ -463,15 +404,6 @@ describe('/tags', () => { }); describe('PUT /tags/:id/assets', () => { - it('should require authentication', async () => { - const tagA = await create(user.accessToken, { name: 'TagA' }); - const { status, body } = await request(app) - .put(`/tags/${tagA.id}/assets`) - .send({ ids: [userAsset.id] }); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const tag = await create(user.accessToken, { name: 'TagA' }); const { status, body } = await request(app) @@ -531,16 +463,6 @@ describe('/tags', () => { }); describe('DELETE /tags/:id/assets', () => { - it('should require authentication', async () => { - const tagA = await create(admin.accessToken, { name: 'TagA' }); - const { status, body } = await request(app) - .delete(`/tags/${tagA}/assets`) - .send({ ids: [userAsset.id] }); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const tagA = await create(user.accessToken, { name: 'TagA' }); await tagAssets( diff --git a/e2e/src/specs/server/api/user-admin.e2e-spec.ts b/e2e/src/specs/server/api/user-admin.e2e-spec.ts index f442d4249d..160c929591 100644 --- a/e2e/src/specs/server/api/user-admin.e2e-spec.ts +++ b/e2e/src/specs/server/api/user-admin.e2e-spec.ts @@ -46,12 +46,6 @@ describe('/admin/users', () => { }); describe('GET /admin/users', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/admin/users`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { status, body } = await request(app) .get(`/admin/users`) @@ -94,12 +88,6 @@ describe('/admin/users', () => { }); describe('POST /admin/users', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post(`/admin/users`).send(createUserDto.user1); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { status, body } = await request(app) .post(`/admin/users`) @@ -109,23 +97,6 @@ describe('/admin/users', () => { expect(body).toEqual(errorDto.forbidden); }); - for (const [key, message] of [ - ['password', 'Invalid input: expected string, received null'], - ['email', 'Invalid input: expected email, received object'], - ['name', 'Invalid input: expected string, received null'], - ['shouldChangePassword', 'Invalid input: expected boolean, received null'], - ['notify', 'Invalid input: expected boolean, received null'], - ] as const) { - it(`should not allow null ${key}`, async () => { - const { status, body } = await request(app) - .post(`/admin/users`) - .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ ...createUserDto.user1, [key]: null }); - expect(status).toBe(400); - expect(body).toEqual(errorDto.validationError([{ path: [key], message }])); - }); - } - it('should accept `isAdmin`', async () => { const { status, body } = await request(app) .post(`/admin/users`) @@ -146,12 +117,6 @@ describe('/admin/users', () => { }); describe('PUT /admin/users/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/admin/users/${uuidDto.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { status, body } = await request(app) .put(`/admin/users/${uuidDto.notFound}`) @@ -160,22 +125,6 @@ describe('/admin/users', () => { expect(body).toEqual(errorDto.forbidden); }); - for (const [key, message] of [ - ['password', 'Invalid input: expected string, received null'], - ['email', 'Invalid input: expected email, received object'], - ['name', 'Invalid input: expected string, received null'], - ['shouldChangePassword', 'Invalid input: expected boolean, received null'], - ] as const) { - it(`should not allow null ${key}`, async () => { - const { status, body } = await request(app) - .put(`/admin/users/${uuidDto.notFound}`) - .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ [key]: null }); - expect(status).toBe(400); - expect(body).toEqual(errorDto.validationError([{ path: [key], message }])); - }); - } - it('should allow a non-admin to become an admin', async () => { const user = await utils.userSetup(admin.accessToken, createUserDto.create('admin2')); const { status, body } = await request(app) @@ -245,12 +194,6 @@ describe('/admin/users', () => { }); describe('PUT /admin/users/:id/preferences', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put(`/admin/users/${userToDelete.userId}/preferences`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should update memories enabled', async () => { const before = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) }); expect(before).toMatchObject({ memories: { enabled: true } }); @@ -282,12 +225,6 @@ describe('/admin/users', () => { }); describe('DELETE /admin/users/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).delete(`/admin/users/${userToDelete.userId}`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { status, body } = await request(app) .delete(`/admin/users/${userToDelete.userId}`) @@ -360,12 +297,6 @@ describe('/admin/users', () => { }); describe('GET /admin/users/:id/calendar-heatmap', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).get(`/admin/users/${nonAdmin.userId}/calendar-heatmap`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require admin permissions', async () => { const { status, body } = await request(app) .get(`/admin/users/${nonAdmin.userId}/calendar-heatmap`) @@ -385,12 +316,6 @@ describe('/admin/users', () => { }); describe('POST /admin/users/:id/restore', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post(`/admin/users/${userToDelete.userId}/restore`); - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - it('should require authorization', async () => { const { status, body } = await request(app) .post(`/admin/users/${userToDelete.userId}/restore`) diff --git a/e2e/src/specs/server/api/user.e2e-spec.ts b/e2e/src/specs/server/api/user.e2e-spec.ts index 2dc789a91b..0e62672f36 100644 --- a/e2e/src/specs/server/api/user.e2e-spec.ts +++ b/e2e/src/specs/server/api/user.e2e-spec.ts @@ -1,34 +1,18 @@ -import { LoginResponseDto, SharedLinkType, deleteUserAdmin, getMyPreferences, getMyUser, login } from '@immich/sdk'; +import { LoginResponseDto, SharedLinkType, getMyUser, login } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; -const userLicense = { - licenseKey: 'IMCL-FF69-TUK1-RWZU-V9Q8-QGQS-S5GC-X4R2-UFK4', - activationKey: - 'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw', -}; - describe('/users', () => { let admin: LoginResponseDto; - let deletedUser: LoginResponseDto; let nonAdmin: LoginResponseDto; beforeAll(async () => { await utils.resetDatabase(); admin = await utils.adminSetup({ onboarding: false }); - - [deletedUser, nonAdmin] = await Promise.all([ - utils.userSetup(admin.accessToken, createUserDto.user1), - utils.userSetup(admin.accessToken, createUserDto.user2), - ]); - - await deleteUserAdmin( - { id: deletedUser.userId, userAdminDeleteDto: {} }, - { headers: asBearerAuth(admin.accessToken) }, - ); + nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user2); }); describe('GET /users/me', () => { @@ -42,55 +26,9 @@ describe('/users', () => { expect(status).toBe(403); expect(body).toEqual(errorDto.forbidden); }); - - it('should get my user', async () => { - const { status, body } = await request(app).get(`/users/me`).set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ - id: admin.userId, - email: 'admin@immich.cloud', - quotaUsageInBytes: 0, - }); - }); - - it('should get my user with license info', async () => { - const { status: licenseStatus } = await request(app) - .put(`/users/me/license`) - .send(userLicense) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - expect(licenseStatus).toBe(200); - const { status, body } = await request(app) - .get(`/users/me`) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ - id: nonAdmin.userId, - email: nonAdmin.userEmail, - quotaUsageInBytes: 0, - license: userLicense, - }); - }); }); describe('PUT /users/me', () => { - it('should update first and last name', async () => { - const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) }); - - const { status, body } = await request(app) - .put(`/users/me`) - .send({ name: 'Name' }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ - ...before, - updatedAt: expect.any(String), - profileChangedAt: expect.any(String), - createdAt: expect.any(String), - name: 'Name', - }); - }); - /** @deprecated */ it('should allow a user to change their password (deprecated)', async () => { const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) }); @@ -112,222 +50,5 @@ describe('/users', () => { expect(token.accessToken).toBeDefined(); }); - - it('should not allow user to change to a taken email', async () => { - const { status, body } = await request(app) - .put(`/users/me`) - .send({ email: 'admin@immich.cloud' }) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - - expect(status).toBe(400); - expect(body).toMatchObject(errorDto.badRequest('Email is not available')); - }); - - it('should update my email', async () => { - const before = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) }); - const { status, body } = await request(app) - .put(`/users/me`) - .send({ email: 'non-admin@immich.cloud' }) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ - ...before, - email: 'non-admin@immich.cloud', - updatedAt: expect.anything(), - createdAt: expect.anything(), - profileChangedAt: expect.anything(), - }); - }); - - it('should update avatar color', async () => { - const { status, body } = await request(app) - .put(`/users/me`) - .send({ avatarColor: 'blue' }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ avatarColor: 'blue' }); - - const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) }); - expect(after).toMatchObject({ avatarColor: 'blue' }); - }); - }); - - describe('PUT /users/me/preferences', () => { - it('should update memories enabled', async () => { - const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(before).toMatchObject({ memories: { enabled: true } }); - - const { status, body } = await request(app) - .put(`/users/me/preferences`) - .send({ memories: { enabled: false } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ memories: { enabled: false } }); - - const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(after).toMatchObject({ memories: { enabled: false } }); - }); - - it('should require an integer for download archive size', async () => { - const { status, body } = await request(app) - .put(`/users/me/preferences`) - .send({ download: { archiveSize: 1_234_567.89 } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(400); - expect(body).toEqual( - errorDto.validationError([ - { path: ['download', 'archiveSize'], message: 'Invalid input: expected int, received number' }, - ]), - ); - }); - - it('should update download archive size', async () => { - const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(before).toMatchObject({ download: { archiveSize: 4 * 2 ** 30 } }); - - const { status, body } = await request(app) - .put(`/users/me/preferences`) - .send({ download: { archiveSize: 1_234_567 } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ download: { archiveSize: 1_234_567 } }); - - const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } }); - }); - - it('should require a boolean for download include embedded videos', async () => { - const { status, body } = await request(app) - .put(`/users/me/preferences`) - .send({ download: { includeEmbeddedVideos: 1_234_567.89 } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(400); - expect(body).toEqual( - errorDto.validationError([ - { path: ['download', 'includeEmbeddedVideos'], message: 'Invalid input: expected boolean, received number' }, - ]), - ); - }); - - it('should update download include embedded videos', async () => { - const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(before).toMatchObject({ download: { includeEmbeddedVideos: false } }); - - const { status, body } = await request(app) - .put(`/users/me/preferences`) - .send({ download: { includeEmbeddedVideos: true } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - - expect(status).toBe(200); - expect(body).toMatchObject({ download: { includeEmbeddedVideos: true } }); - - const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(after).toMatchObject({ download: { includeEmbeddedVideos: true } }); - }); - - it('should update minimum face count to display people', async () => { - const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(before).toMatchObject({ people: { minimumFaces: 3 } }); - - const { status, body } = await request(app) - .put('/users/me/preferences') - .send({ people: { minimumFaces: 2 } }) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ people: { minimumFaces: 2 } }); - - const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); - expect(after).toMatchObject({ people: { minimumFaces: 2 } }); - }); - }); - - describe('GET /users/:id', () => { - it('should get the user', async () => { - const { status, body } = await request(app) - .get(`/users/${admin.userId}`) - .set('Authorization', `Bearer ${admin.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ - id: admin.userId, - email: 'admin@immich.cloud', - }); - - expect(body).not.toMatchObject({ - shouldChangePassword: expect.anything(), - storageLabel: expect.anything(), - }); - }); - }); - - describe('GET /server/license', () => { - it('should return the user license', async () => { - await request(app) - .put('/users/me/license') - .set('Authorization', `Bearer ${nonAdmin.accessToken}`) - .send(userLicense); - const { status, body } = await request(app) - .get('/users/me/license') - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - expect(status).toBe(200); - expect(body).toEqual({ - ...userLicense, - activatedAt: expect.any(String), - }); - }); - }); - - describe('PUT /users/me/license', () => { - it('should set the user license', async () => { - const { status, body } = await request(app) - .put(`/users/me/license`) - .send(userLicense) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ ...userLicense, activatedAt: expect.any(String) }); - expect(status).toBe(200); - expect(body).toEqual({ ...userLicense, activatedAt: expect.any(String) }); - const { body: licenseBody } = await request(app) - .get('/users/me/license') - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - expect(licenseBody).toEqual({ ...userLicense, activatedAt: expect.any(String) }); - }); - - it('should reject license not starting with IMCL-', async () => { - const { status, body } = await request(app) - .put('/users/me/license') - .set('Authorization', `Bearer ${nonAdmin.accessToken}`) - .send({ licenseKey: 'IMSV-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD', activationKey: 'activationKey' }); - expect(status).toBe(400); - expect(body.message).toBe('Invalid license key'); - }); - - it('should reject license with invalid activation key', async () => { - const { status, body } = await request(app) - .put('/users/me/license') - .set('Authorization', `Bearer ${nonAdmin.accessToken}`) - .send({ licenseKey: userLicense.licenseKey, activationKey: `invalid${userLicense.activationKey}` }); - expect(status).toBe(400); - expect(body.message).toBe('Invalid license key'); - }); - }); - - describe('DELETE /users/me/license', () => { - it('should require authentication', async () => { - const { status } = await request(app).put(`/users/me/license`); - expect(status).toEqual(401); - }); - - it('should delete the user license', async () => { - const { status } = await request(app) - .delete(`/users/me/license`) - .set('Authorization', `Bearer ${nonAdmin.accessToken}`); - expect(status).toBe(204); - }); }); }); diff --git a/mise.lock b/mise.lock index a4b395500b..836712e0fd 100644 --- a/mise.lock +++ b/mise.lock @@ -104,17 +104,17 @@ version = "7.1.3-6" backend = "github:jellyfin/jellyfin-ffmpeg" [tools."github:jellyfin/jellyfin-ffmpeg".options] -asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz" +asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz" -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"] -checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876" +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"] +checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879" -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"] -checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876" +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"] +checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879" [[tools."github:jellyfin/jellyfin-ffmpeg"]] version = "7.1.3-6" @@ -133,17 +133,17 @@ version = "7.1.3-6" backend = "github:jellyfin/jellyfin-ffmpeg" [tools."github:jellyfin/jellyfin-ffmpeg".options] -asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz" +asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz" -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"] -checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879" +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"] +checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876" -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"] -checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879" +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"] +checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876" [[tools."github:webassembly/binaryen"]] version = "version_124" @@ -291,43 +291,43 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12. url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602547" [[tools.pnpm]] -version = "11.17.0" +version = "11.20.0" backend = "aqua:pnpm/pnpm" [tools.pnpm."platforms.linux-arm64"] -checksum = "sha256:730d17de742a3efbb020ba91d7acfc0456c6ba6ad1cd8eb49f4c229fe9f504d3" -url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-linux-arm64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422123" +checksum = "sha256:f00fc2041bb41742b7943bf2bb24183ad20320e8384824a8031eb94edf2f57a5" +url = "https://github.com/pnpm/pnpm/releases/download/v11.20.0/pnpm-linux-arm64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/500142378" provenance = "github-attestations" [tools.pnpm."platforms.linux-arm64-musl"] -checksum = "sha256:cc072c0e7bdd290f52fb9afdcea778ede69f96f0f4dae63623163fc11d4beec3" -url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-linux-arm64-musl.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422121" +checksum = "sha256:2a21235d3f0fbfbcb9e530b9a98f2c64dcee95d1d5d1c2d04428255c7b85f32c" +url = "https://github.com/pnpm/pnpm/releases/download/v11.20.0/pnpm-linux-arm64-musl.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/500142372" provenance = "github-attestations" [tools.pnpm."platforms.linux-x64"] -checksum = "sha256:bdb1db01bf0f757495405a59a09c5c287f315889dc98d3b14bc374b9fe43a0bf" -url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-linux-x64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422122" +checksum = "sha256:b4ad6ad2b21db2f8cd50af416c3aa148ba704c31c84893f465a770a01c2c4572" +url = "https://github.com/pnpm/pnpm/releases/download/v11.20.0/pnpm-linux-x64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/500142376" provenance = "github-attestations" [tools.pnpm."platforms.linux-x64-musl"] -checksum = "sha256:db2e4eeecab336bd41bbbc7a39626b166f1a15bb189efcf6a1dafdf159f23fe7" -url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-linux-x64-musl.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422116" +checksum = "sha256:db046881c027f1a2d69e9a530a21a62c2bced4ba8ff437e7300426ae56951fe4" +url = "https://github.com/pnpm/pnpm/releases/download/v11.20.0/pnpm-linux-x64-musl.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/500142377" provenance = "github-attestations" [tools.pnpm."platforms.macos-arm64"] -checksum = "sha256:1e9a35d76f9d382af365d95c64f3abf7815e24350653f5bb1a37e4546265bc2b" -url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-darwin-arm64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422117" +checksum = "sha256:4bc97fea72e5c92eec1fefc8d410c35d01e0d0d52f3160c59f38c32db58b5cd2" +url = "https://github.com/pnpm/pnpm/releases/download/v11.20.0/pnpm-darwin-arm64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/500142373" provenance = "github-attestations" [tools.pnpm."platforms.windows-x64"] -checksum = "sha256:3025c174a6dcffec14a071a5c854cb0c350a09a8c7c2857a75f75cc7b29b939c" -url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-win32-x64.zip" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422118" +checksum = "sha256:ea2528bdc3d96a1ff3c35587dc48ca692b39d77f08f26df4adeaaa9eb427024e" +url = "https://github.com/pnpm/pnpm/releases/download/v11.20.0/pnpm-win32-x64.zip" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/500142371" provenance = "github-attestations" [[tools.terragrunt]] diff --git a/mise.toml b/mise.toml index ad7fdf5a90..0039dbac2d 100644 --- a/mise.toml +++ b/mise.toml @@ -16,7 +16,7 @@ config_roots = [ [tools] node = "24.15.0" -pnpm = "11.17.0" +pnpm = "11.20.0" terragrunt = "1.1.1" opentofu = "1.12.5" "npm:@openapitools/openapi-generator-cli" = "2.40.1" diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 7e314e8d81..fb5d96697b 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -97,6 +97,18 @@ dart_code_metrics: exclude-paths: - 'lib/utils/debug_print.dart' severity: perf + - name: tr + description: 'Use context.t. or StaticTranslations.instance. instead of tr().' + - type: String + entries: + - name: tr + description: 'Use context.t. or StaticTranslations.instance. instead of String.tr().' + - name: plural + description: 'Use the generated plural accessor instead of String.plural().' + - type: Text + entries: + - name: tr + description: 'Use Text(context.t.) instead of Text().tr().' # All rules from "recommended" preset # Show potential errors # - avoid-cascade-after-if-null diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImagesImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImagesImpl.kt index 7b49d8ca67..af953612cf 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImagesImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImagesImpl.kt @@ -33,12 +33,28 @@ data class Request( val callback: (Result?>) -> Unit ) +/** Set [exactSize] to decode at the smallest size that covers [target] without upscaling. */ @RequiresApi(Build.VERSION_CODES.Q) -inline fun ImageDecoder.Source.decodeBitmap(target: Size = Size(0, 0)): Bitmap { +inline fun ImageDecoder.Source.decodeBitmap(target: Size = Size(0, 0), exactSize: Boolean = false): Bitmap { return ImageDecoder.decodeBitmap(this) { decoder, info, _ -> if (target.width > 0 && target.height > 0) { - val sample = max(1, min(info.size.width / target.width, info.size.height / target.height)) - decoder.setTargetSampleSize(sample) + if (exactSize) { + val fillScale = max( + target.width.toDouble() / info.size.width, + target.height.toDouble() / info.size.height + ) + val scale = min(1.0, fillScale) + if (scale < 1) { + val width = ceil(info.size.width * scale).toInt() + val height = ceil(info.size.height * scale).toInt() + if (width > 0 && height > 0) { + decoder.setTargetSize(width, height) + } + } + } else { + val sample = max(1, min(info.size.width / target.width, info.size.height / target.height)) + decoder.setTargetSampleSize(sample) + } } decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE decoder.setTargetColorSpace(ColorSpace.get(ColorSpace.Named.SRGB)) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt index 1623c9cb09..cdc82c871c 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImagesImpl.kt @@ -5,6 +5,7 @@ import android.graphics.ImageDecoder import android.os.Build import android.os.CancellationSignal import android.os.OperationCanceledException +import android.util.Size import androidx.exifinterface.media.ExifInterface import app.alextran.immich.INITIAL_BUFFER_SIZE import app.alextran.immich.NativeBuffer @@ -79,6 +80,8 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi { url: String, requestId: Long, preferEncoded: Boolean, + width: Long?, + height: Long?, callback: (Result?>) -> Unit ) { val signal = CancellationSignal() @@ -102,13 +105,21 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi { if (!preferEncoded && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { decodeExecutor.execute { val res = if (signal.isCanceled) null else try { - val bitmap = ImageDecoder.createSource(NativeBuffer.wrap(buffer.pointer, buffer.offset)).decodeBitmap() // The embedded preview a raw decodes to has no orientation, so read the container's. val orientation = if (isRawMime(contentType)) { readRawOrientation(NativeBuffer.wrap(buffer.pointer, buffer.offset), buffer.offset) } else { ExifInterface.ORIENTATION_NORMAL } + val target = when (orientation) { + ExifInterface.ORIENTATION_TRANSPOSE, + ExifInterface.ORIENTATION_ROTATE_90, + ExifInterface.ORIENTATION_TRANSVERSE, + ExifInterface.ORIENTATION_ROTATE_270 -> Size(height?.toInt() ?: 0, width?.toInt() ?: 0) + else -> Size(width?.toInt() ?: 0, height?.toInt() ?: 0) + } + val source = ImageDecoder.createSource(NativeBuffer.wrap(buffer.pointer, buffer.offset)) + val bitmap = source.decodeBitmap(target, exactSize = true) if (orientation == ExifInterface.ORIENTATION_NORMAL || orientation == ExifInterface.ORIENTATION_UNDEFINED) { bitmap.toNativeBuffer() } else { diff --git a/mobile/bin/generate_keys.dart b/mobile/bin/generate_keys.dart index 1ce643be12..160f2c5b5c 100644 --- a/mobile/bin/generate_keys.dart +++ b/mobile/bin/generate_keys.dart @@ -97,6 +97,15 @@ const _kIntParamNames = [ 'quantity', ]; +const _kParamTypeOverrides = { + 'advanced_settings_clear_image_cache_success.size': 'String', + 'backup_controller_page_storage_format.total': 'String', + 'backup_controller_page_storage_format.used': 'String', + 'cleanup_found_assets_with_size.size': 'String', +}; + +final _usedParamTypeOverrides = {}; + void main() async { final sourceFile = File('../i18n/en.json'); if (!await sourceFile.exists()) { @@ -143,6 +152,11 @@ class TranslationParam { Future _generateTranslations(Map translations, File output) async { final root = _buildTranslationTree('', translations); + final staleOverrides = _kParamTypeOverrides.keys.toSet().difference(_usedParamTypeOverrides); + if (staleOverrides.isNotEmpty) { + throw StateError('_kParamTypeOverrides has entries matching no plain {placeholder}: ${staleOverrides.join(', ')}'); + } + final buffer = StringBuffer(''' // DO NOT EDIT. This is code generated via generate_keys.dart @@ -194,21 +208,22 @@ class Translations extends _BaseTranslations { await output.writeAsString(buffer.toString()); } -TranslationNode _buildTranslationTree(String key, dynamic value) { +TranslationNode _buildTranslationTree(String key, dynamic value, [String parentKey = '']) { + final fullKey = parentKey.isEmpty ? key : '$parentKey.$key'; if (value is Map) { final children = {}; for (final entry in value.entries) { - children[entry.key] = _buildTranslationTree(entry.key, entry.value); + children[entry.key] = _buildTranslationTree(entry.key, entry.value, fullKey); } return TranslationNode(key: key, children: children); } else { final stringValue = value.toString(); - final params = _extractParams(stringValue); + final params = _extractParams(stringValue, fullKey); return TranslationNode(key: key, value: stringValue, params: params); } } -List _extractParams(String value) { +List _extractParams(String value, String fullKey) { final params = {}; final icuRegex = RegExp(r'\{(\w+),\s*(plural|select|number|date|time)([^}]*(?:\{[^}]*\}[^}]*)*)\}'); @@ -264,8 +279,11 @@ List _extractParams(String value) { continue; } - String type; - if (_kIntParamNames.contains(name.toLowerCase())) { + final String type; + if (_kParamTypeOverrides['$fullKey.$name'] case final override?) { + type = override; + _usedParamTypeOverrides.add('$fullKey.$name'); + } else if (_kIntParamNames.contains(name.toLowerCase())) { type = 'int'; } else { type = 'Object'; diff --git a/mobile/integration_test/failed_sync_resume_e2e_test.dart b/mobile/integration_test/failed_sync_resume_e2e_test.dart new file mode 100644 index 0000000000..255074df93 --- /dev/null +++ b/mobile/integration_test/failed_sync_resume_e2e_test.dart @@ -0,0 +1,101 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/background_worker.service.dart'; +import 'package:immich_mobile/domain/utils/background_sync.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/main.dart' as app; +import 'package:immich_mobile/platform/background_worker_api.g.dart'; +import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/bootstrap.dart'; +import 'package:immich_mobile/wm_executor.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'test_utils/fake_immich_server.dart'; + +// Issue #28082 end-to-end: a resume after a sync froze mid-flight starts a fresh sync. +// Kept in its own file on purpose: run after the failed_sync_resume tests in one process, +// the resume sync starves past its window - a cross-test interaction that survived pool, +// sqlite and bg-worker audits unnamed. One file per process is device-proven green. +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; + + late Drift drift; + late FakeImmichServer server; + + // main()'s formula with a higher floor: a wedged frozen worker plus a full local + // sync must not starve the fresh resume sync out of its 25s window on 4 cores. + final poolSize = max(Platform.numberOfProcessors - 1, 8); + + setUpAll(() async { + await app.initApp(); + (drift, _) = await Bootstrap.initDomain(); + // A background-worker schedule persisted by real app use on this device can + // launch a second engine mid-file (own isolate pool + full sync) and starve + // these tests on a small device. Unregister it for the whole run. + await BackgroundWorkerFgService(BackgroundWorkerFgHostApi()).disable(); + }); + + setUp(() async { + // A task completing while dispose tears the pool down re-warms it (_schedule + // on the cleared pool re-creates workers), and init on a warm pool is silently + // ignored - poolSize would never apply. Reset first, then verify it took. + await workerManagerPatch.dispose(); + await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: poolSize); + expect( + workerManagerPatch.pool.length, + poolSize, + reason: 'init was ignored: a straggler from the previous test re-warmed the pool', + ); + server = await FakeImmichServer.start(); + await ApiService().resolveAndSetEndpoint(server.endpoint); + await drift.delete(drift.userEntity).go(); + }); + + tearDown(() async { + // Close the server first so any held-open sync stream ends and its isolate unwinds, + // then drain the pool - otherwise dispose waits on the frozen read. + await server.close(); + await workerManagerPatch.dispose(); + }); + + testWidgets('a resume after a sync froze mid-flight starts a fresh sync', (tester) async { + final manager = BackgroundSyncManager(); + // Not disposed on purpose: driftOverride closes the drift on dispose, and that + // drift belongs to setUpAll. The container only holds the shared drift and a + // fire-and-forget resume; the frozen isolates + server are drained by tearDown. + final container = ProviderContainer( + overrides: [driftProvider.overrideWith(driftOverride(drift)), backgroundSyncProvider.overrideWithValue(manager)], + ); + + // A first sync opens /sync/stream and never finishes - the frozen state a + // suspended sync isolate is left in. Holding the stream open keeps + // _syncTask non-null, exactly as it is across an iOS process suspension. + unawaited(manager.syncRemote()); + await server + .streamOpenedNth(1) + .timeout(const Duration(seconds: 30), onTimeout: () => fail('first sync isolate never opened /sync/stream')); + + // The lifecycle then goes background -> foreground. handleAppResume runs the + // resume sync exactly once. On the buggy build it hangs on the stale task's + // future, so it is not awaited here. + final notifier = container.read(appStateProvider.notifier); + await notifier.handleAppPause(); + unawaited(notifier.handleAppResume()); + + await server + .streamOpenedNth(2) + .timeout( + const Duration(seconds: 25), + onTimeout: () => fail('resume did not start a fresh remote sync - the stale frozen sync blocked it (#28082)'), + ); + expect(server.streamOpenCount, greaterThanOrEqualTo(2)); + }); +} diff --git a/mobile/integration_test/failed_sync_resume_test.dart b/mobile/integration_test/failed_sync_resume_test.dart new file mode 100644 index 0000000000..c23319dffe --- /dev/null +++ b/mobile/integration_test/failed_sync_resume_test.dart @@ -0,0 +1,133 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/services/background_worker.service.dart'; +import 'package:immich_mobile/domain/utils/background_sync.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/main.dart' as app; +import 'package:immich_mobile/platform/background_worker_api.g.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/bootstrap.dart'; +import 'package:immich_mobile/wm_executor.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'test_utils/fake_immich_server.dart'; + +// Issue #28082: a remote sync in-flight when the app is backgrounded stays referenced +// but frozen across the suspension. On resume the app drops the stale task +// (cancelResumeSyncs) and starts a fresh sync. +// +// Device/emulator tests: real worker isolates + a real drift db + a loopback fake server +// (same pattern as background_sync_teardown_test). The mobile integration-test CI job is +// disabled in test.yml, so like Mert's teardown test this is a local/on-device guard. +// +// The end-to-end resume test lives in failed_sync_resume_e2e_test.dart - one process +// per file keeps it clear of the cross-test interaction described there. +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; + + late Drift drift; + late FakeImmichServer server; + + // main()'s formula with a higher floor: a wedged frozen worker plus a full local + // sync must not starve the fresh resume sync out of its 25s window on 4 cores. + final poolSize = max(Platform.numberOfProcessors - 1, 8); + + setUpAll(() async { + await app.initApp(); + (drift, _) = await Bootstrap.initDomain(); + // A background-worker schedule persisted by real app use on this device can + // launch a second engine mid-file (own isolate pool + full sync) and starve + // these tests on a small device. Unregister it for the whole run. + await BackgroundWorkerFgService(BackgroundWorkerFgHostApi()).disable(); + }); + + setUp(() async { + // A task completing while dispose tears the pool down re-warms it (_schedule + // on the cleared pool re-creates workers), and init on a warm pool is silently + // ignored - poolSize would never apply. Reset first, then verify it took. + await workerManagerPatch.dispose(); + await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: poolSize); + expect( + workerManagerPatch.pool.length, + poolSize, + reason: 'init was ignored: a straggler from the previous test re-warmed the pool', + ); + server = await FakeImmichServer.start(); + await ApiService().resolveAndSetEndpoint(server.endpoint); + await drift.delete(drift.userEntity).go(); + }); + + tearDown(() async { + // Close the server first so any held-open sync stream ends and its isolate unwinds, + // then drain the pool - otherwise dispose waits on the frozen read. + await server.close(); + await workerManagerPatch.dispose(); + }); + + // Self-contained (bare manager, no fire-and-forget resume), so it runs first: its + // frozen syncs are fully drained by tearDown. + testWidgets('a cancelled sync task does not clear the slot of the fresh task that superseded it', (tester) async { + final manager = BackgroundSyncManager(); + + // First sync opens /sync/stream and is held open - the frozen suspended state. + unawaited(manager.syncRemote()); + await server + .streamOpenedNth(1) + .timeout(const Duration(seconds: 30), onTimeout: () => fail('first sync isolate never opened /sync/stream')); + + // Resume drops the stale task then immediately starts a fresh one, exactly as + // _handleBetaTimelineResume does. cancelResumeSyncs cancels the first task; its + // completion chain then fires and must NOT null the fresh task's slot. + unawaited(manager.cancelResumeSyncs()); + unawaited(manager.syncRemote()); + await server + .streamOpenedNth(2) + .timeout(const Duration(seconds: 30), onTimeout: () => fail('fresh sync isolate never opened /sync/stream')); + + // A third sync stream only opens if the cancelled task cleared the fresh slot, + // letting the dedupe guard start a redundant sync. + var thirdOpened = false; + unawaited(server.streamOpenedNth(3).then((_) => thirdOpened = true)); + + // Let the cancelled task's completion chain settle, then ask to sync again. + await Future.delayed(const Duration(milliseconds: 200)); + unawaited(manager.syncRemote()); + await Future.delayed(const Duration(seconds: 3)); + + expect( + thirdOpened, + isFalse, + reason: 'the cancelled task cleared the fresh task slot, so a redundant third sync started (#28082 clobber)', + ); + expect(server.streamOpenCount, 2); + }); + + // The false-error-flash fix: a task cancelled by cancelResumeSyncs completes with a + // CanceledError, which is not a sync failure and must not reach onRemoteSyncError. + // Before the filter the stale task reported an error right after the fresh sync + // started, so the status UI showed a failure for the whole healthy run. + testWidgets('a cancelled sync does not report a false error to the status callbacks', (tester) async { + final errors = []; + final manager = BackgroundSyncManager(onRemoteSyncError: errors.add); + + // Hold the stream open so the task is genuinely in-flight when it is cancelled. + unawaited(manager.syncRemote()); + await server + .streamOpenedNth(1) + .timeout(const Duration(seconds: 30), onTimeout: () => fail('sync isolate never opened /sync/stream')); + + await manager.cancelResumeSyncs(); + // Let the cancelled task's completion chain run before checking the callbacks. + await Future.delayed(const Duration(milliseconds: 100)); + + expect( + errors, + isEmpty, + reason: 'a cancelled task is not a real error; its CanceledError must not fire onRemoteSyncError', + ); + }); +} diff --git a/mobile/integration_test/module_login/login_input_validation_test.dart b/mobile/integration_test/module_login/login_input_validation_test.dart index 1b47b6d8cc..504940f643 100644 --- a/mobile/integration_test/module_login/login_input_validation_test.dart +++ b/mobile/integration_test/module_login/login_input_validation_test.dart @@ -1,5 +1,5 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import '../test_utils/general_helper.dart'; @@ -7,45 +7,32 @@ void main() async { await ImmichTestHelper.initialize(); group("Login input validation test", () { - immichWidgetTest("Test leading/trailing whitespace", - (tester, helper) async { + immichWidgetTest("Test leading/trailing whitespace", (tester, helper) async { await helper.loginHelper.waitForLoginScreen(); await helper.loginHelper.acknowledgeNewServerVersion(); - await helper.loginHelper.enterCredentials( - email: " demo@immich.app", - ); + await helper.loginHelper.enterCredentials(email: " demo@immich.app"); await tester.pump(const Duration(milliseconds: 300)); - expect( - find.text("login_form_err_leading_whitespace".tr()), - findsOneWidget, - ); + expect(find.text(StaticTranslations.instance.login_form_err_leading_whitespace), findsOneWidget); - await helper.loginHelper.enterCredentials( - email: "demo@immich.app ", - ); + await helper.loginHelper.enterCredentials(email: "demo@immich.app "); await tester.pump(const Duration(milliseconds: 300)); - expect( - find.text("login_form_err_trailing_whitespace".tr()), - findsOneWidget, - ); + expect(find.text(StaticTranslations.instance.login_form_err_trailing_whitespace), findsOneWidget); }); immichWidgetTest("Test invalid email", (tester, helper) async { await helper.loginHelper.waitForLoginScreen(); await helper.loginHelper.acknowledgeNewServerVersion(); - await helper.loginHelper.enterCredentials( - email: "demo.immich.app", - ); + await helper.loginHelper.enterCredentials(email: "demo.immich.app"); await tester.pump(const Duration(milliseconds: 300)); - expect(find.text("login_form_err_invalid_email".tr()), findsOneWidget); + expect(find.text(StaticTranslations.instance.login_form_err_invalid_email), findsOneWidget); }); }); } diff --git a/mobile/integration_test/test_utils/fake_immich_server.dart b/mobile/integration_test/test_utils/fake_immich_server.dart index c434f83bc5..7c63098c52 100644 --- a/mobile/integration_test/test_utils/fake_immich_server.dart +++ b/mobile/integration_test/test_utils/fake_immich_server.dart @@ -10,6 +10,8 @@ class FakeImmichServer { final (int, int, int) version; final Completer _streamOpened = Completer(); + final List _streamOpens = []; + final Map> _openWaiters = {}; int ackRequests = 0; @@ -18,6 +20,17 @@ class FakeImmichServer { /// Resolves when the sync isolate opens `POST /sync/stream`. Future get streamOpened => _streamOpened.future; + /// How many `/sync/stream` requests have opened so far. + int get streamOpenCount => _streamOpens.length; + + /// Resolves when the [n]-th (1-indexed) `/sync/stream` opens. + Future streamOpenedNth(int n) { + if (_streamOpens.length >= n) { + return Future.value(_streamOpens[n - 1]); + } + return (_openWaiters[n] ??= Completer()).future; + } + static Future start({(int, int, int) version = (3, 0, 0)}) async { final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); final fake = FakeImmichServer._(server, version); @@ -58,13 +71,17 @@ class FakeImmichServer { request.response ..statusCode = HttpStatus.ok ..headers.contentType = ContentType('application', 'jsonlines+json') - ..contentLength = -1 // chunked: stays open to stream incrementally + ..contentLength = + -1 // chunked: stays open to stream incrementally ..bufferOutput = false; // Flush headers so the client's send() resolves and enters its read loop. await request.response.flush(); + final stream = SyncStream._(request.response); + _streamOpens.add(stream); if (!_streamOpened.isCompleted) { - _streamOpened.complete(SyncStream._(request.response)); + _streamOpened.complete(stream); } + _openWaiters.remove(_streamOpens.length)?.complete(stream); } Future _respondJson(HttpRequest request, Object body) async { @@ -83,8 +100,8 @@ class FakeImmichServer { } Future close() async { - if (_streamOpened.isCompleted) { - await (await _streamOpened.future).close(); + for (final stream in _streamOpens) { + await stream.close(); } await _server.close(force: true); } diff --git a/mobile/integration_test/test_utils/login_helper.dart b/mobile/integration_test/test_utils/login_helper.dart index cfbc5a9214..ad61b77a53 100644 --- a/mobile/integration_test/test_utils/login_helper.dart +++ b/mobile/integration_test/test_utils/login_helper.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'general_helper.dart'; @@ -23,11 +23,7 @@ class ImmichTestLoginHelper { return true; } - Future enterCredentials({ - String server = "", - String email = "", - String password = "", - }) async { + Future enterCredentials({String server = "", String email = "", String password = ""}) async { final loginForms = find.byType(TextFormField); await tester.enterText(loginForms.at(0), email); @@ -44,49 +40,30 @@ class ImmichTestLoginHelper { } Future enterCredentialsOf(LoginCredentials credentials) async { - await enterCredentials( - server: credentials.server, - email: credentials.email, - password: credentials.password, - ); + await enterCredentials(server: credentials.server, email: credentials.email, password: credentials.password); } Future pressLoginButton() async { - await pumpUntilFound( - tester, - find.textContaining("login_form_button_text".tr()), - ); - final button = find.textContaining("login_form_button_text".tr()); + await pumpUntilFound(tester, find.textContaining(StaticTranslations.instance.login)); + final button = find.textContaining(StaticTranslations.instance.login); await tester.tap(button); } Future assertLoginSuccess() async { - await pumpUntilFound(tester, find.text("home_page_building_timeline".tr())); + await pumpUntilFound(tester, find.text(StaticTranslations.instance.home_page_building_timeline)); } Future assertLoginFailed() async { - await pumpUntilFound(tester, find.text("login_form_failed_login".tr())); + await pumpUntilFound(tester, find.text(StaticTranslations.instance.login_form_failed_login)); } } enum LoginCredentials { - testInstance( - "https://flutter-int-test.preview.immich.app", - "demo@immich.app", - "demo", - ), + testInstance("https://flutter-int-test.preview.immich.app", "demo@immich.app", "demo"), - testInstanceButWithWrongPassword( - "https://flutter-int-test.preview.immich.app", - "demo@immich.app", - "wrong", - ), + testInstanceButWithWrongPassword("https://flutter-int-test.preview.immich.app", "demo@immich.app", "wrong"), - wrongInstanceUrl( - "https://does-not-exist.preview.immich.app", - "demo@immich.app", - "demo", - ); + wrongInstanceUrl("https://does-not-exist.preview.immich.app", "demo@immich.app", "demo"); const LoginCredentials(this.server, this.email, this.password); diff --git a/mobile/ios/Runner/Images/RemoteImagesImpl.swift b/mobile/ios/Runner/Images/RemoteImagesImpl.swift index de1f6dec89..b4b70f0b30 100644 --- a/mobile/ios/Runner/Images/RemoteImagesImpl.swift +++ b/mobile/ios/Runner/Images/RemoteImagesImpl.swift @@ -1,5 +1,6 @@ import Accelerate import Flutter +import ImageIO import MobileCoreServices import Photos @@ -27,21 +28,21 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue), renderingIntent: .perceptual )! - private static let decodeOptions = [ + private static let decodeOptions: [NSString: Bool] = [ kCGImageSourceShouldCache: false, kCGImageSourceShouldCacheImmediately: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceCreateThumbnailFromImageAlways: true - ] as CFDictionary + ] - func requestImage(url: String, requestId: Int64, preferEncoded: Bool, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) { + func requestImage(url: String, requestId: Int64, preferEncoded: Bool, width: Int64?, height: Int64?, completion: @escaping (Result<[String : Int64]?, any Error>) -> Void) { var urlRequest = URLRequest(url: URL(string: url)!) urlRequest.cachePolicy = .returnCacheDataElseLoad let request = RemoteImageRequest(id: requestId, completion: completion) let task = URLSessionManager.shared.session.dataTask(with: urlRequest) { data, response, error in - Self.handleCompletion(request: request, encoded: preferEncoded, data: data, response: response, error: error) + Self.handleCompletion(request: request, encoded: preferEncoded, width: width, height: height, data: data, response: response, error: error) } request.task = task @@ -49,7 +50,7 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { task.resume() } - private static func handleCompletion(request: RemoteImageRequest, encoded: Bool, data: Data?, response: URLResponse?, error: Error?) { + private static func handleCompletion(request: RemoteImageRequest, encoded: Bool, width: Int64?, height: Int64?, data: Data?, response: URLResponse?, error: Error?) { if request.isCancelled { return request.completion(ImageProcessing.cancelledResult) } @@ -87,8 +88,17 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { return request.completion(ImageProcessing.cancelledResult) } - guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil), - let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, decodeOptions) else { + guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else { + registry.remove(requestId: request.id) + return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil))) + } + + var options: [NSString: Any] = decodeOptions + if let maxPixelSize = targetThumbnailRenderSize(imageSource: imageSource, width: width, height: height) { + options[kCGImageSourceThumbnailMaxPixelSize] = maxPixelSize + } + + guard let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else { registry.remove(requestId: request.id) return request.completion(.failure(PigeonError(code: "", message: "Failed to decode image for request", details: nil))) } @@ -120,6 +130,34 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi { } } + /// Returns the longest rendered edge needed to cover the requested size. + private static func targetThumbnailRenderSize(imageSource: CGImageSource, width: Int64?, height: Int64?) -> Int? { + guard let width, + let height, + width > 0, + height > 0, + let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any], + let pixelWidth = properties[kCGImagePropertyPixelWidth] as? NSNumber, + let pixelHeight = properties[kCGImagePropertyPixelHeight] as? NSNumber else { + return nil + } + + let orientation = (properties[kCGImagePropertyOrientation] as? NSNumber) + .flatMap { CGImagePropertyOrientation(rawValue: $0.uint32Value) } ?? .up + let swapsDimensions: Bool + switch orientation { + case .leftMirrored, .right, .rightMirrored, .left: + swapsDimensions = true + default: + swapsDimensions = false + } + let sourceWidth = swapsDimensions ? pixelHeight.doubleValue : pixelWidth.doubleValue + let sourceHeight = swapsDimensions ? pixelWidth.doubleValue : pixelHeight.doubleValue + let fillScale = max(Double(width) / sourceWidth, Double(height) / sourceHeight) + let scale = min(1, fillScale) + return scale < 1 ? Int(ceil(max(sourceWidth * scale, sourceHeight * scale))) : nil + } + func cancelRequest(requestId: Int64) { Self.registry.remove(requestId: requestId)?.cancel() } diff --git a/mobile/lib/constants/aspect_ratios.dart b/mobile/lib/constants/aspect_ratios.dart index 9ad7b8c739..49165e0913 100644 --- a/mobile/lib/constants/aspect_ratios.dart +++ b/mobile/lib/constants/aspect_ratios.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; -class CropAspectRatio { - final int? numerator; - final int? denominator; +part 'aspect_ratios.freezed.dart'; - final String? customLabel; - final IconData? icon; +@freezed +abstract class CropAspectRatio with _$CropAspectRatio { + const CropAspectRatio._(); - const CropAspectRatio({this.numerator, this.denominator, this.customLabel, this.icon}); + const factory CropAspectRatio({int? numerator, int? denominator, String? customLabel, IconData? icon}) = + _CropAspectRatio; static const free = CropAspectRatio(customLabel: "Free", icon: Icons.crop_free); static const original = CropAspectRatio(customLabel: "Original", icon: Icons.crop_original); @@ -22,24 +23,6 @@ class CropAspectRatio { CropAspectRatio get flipped { return CropAspectRatio(numerator: denominator, denominator: numerator, customLabel: customLabel, icon: icon); } - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is CropAspectRatio && - other.numerator == numerator && - other.denominator == denominator && - other.customLabel == customLabel && - other.icon == icon; - } - - @override - int get hashCode { - return numerator.hashCode ^ denominator.hashCode ^ customLabel.hashCode ^ icon.hashCode; - } } const aspectRatioFree = CropAspectRatio.free; diff --git a/mobile/lib/domain/models/feature_message.model.dart b/mobile/lib/domain/models/feature_message.model.dart index b3c9c459d3..817076c0e9 100644 --- a/mobile/lib/domain/models/feature_message.model.dart +++ b/mobile/lib/domain/models/feature_message.model.dart @@ -1,21 +1,40 @@ import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/utils/semver.dart'; -class FeatureHighlight { +enum FeatureHighlight { + shareQuality(image: 'assets/feature_message/share_quality.webp'), + slideshow(image: 'assets/feature_message/slideshow.webp'), + recentlyAdded(image: 'assets/feature_message/recently_added.webp'), + ocr(image: 'assets/feature_message/ocr.webp'), + openInImmich(image: 'assets/feature_message/open_in_immich.webp', platform: [.android]), + uploadToAlbum(); + /// Asset path of the feature screenshot, or null to show a placeholder. final String? image; - final String titleKey; - final String bodyKey; final List platform; - const FeatureHighlight({ - this.image, - required this.titleKey, - required this.bodyKey, - this.platform = const [.iOS, .android], - }); + const FeatureHighlight({this.image, this.platform = const [.iOS, .android]}); bool get isVisibleOnCurrentPlatform => platform.contains(defaultTargetPlatform); + + String title(Translations t) => switch (this) { + FeatureHighlight.shareQuality => t.share_quality_title, + FeatureHighlight.slideshow => t.slideshow_title, + FeatureHighlight.recentlyAdded => t.recently_added_title, + FeatureHighlight.ocr => t.ocr_title, + FeatureHighlight.openInImmich => t.open_in_immich_title, + FeatureHighlight.uploadToAlbum => t.upload_to_album_title, + }; + + String body(Translations t) => switch (this) { + FeatureHighlight.shareQuality => t.share_quality_body, + FeatureHighlight.slideshow => t.slideshow_body, + FeatureHighlight.recentlyAdded => t.recently_added_body, + FeatureHighlight.ocr => t.ocr_body, + FeatureHighlight.openInImmich => t.open_in_immich_body, + FeatureHighlight.uploadToAlbum => t.upload_to_album_body, + }; } /// The release this batch of highlights was authored for. Content-defined: @@ -24,31 +43,4 @@ const featureMessageRelease = SemVer(major: 3, minor: 0, patch: 0); /// Highlights relevant to the current platform. List get visibleFeatureMessageHighlights => - featureMessageHighlights.where((h) => h.isVisibleOnCurrentPlatform).toList(); - -const List featureMessageHighlights = [ - FeatureHighlight( - image: 'assets/feature_message/share_quality.webp', - titleKey: 'share_quality_title', - bodyKey: 'share_quality_body', - ), - FeatureHighlight( - image: 'assets/feature_message/slideshow.webp', - titleKey: 'slideshow_title', - bodyKey: 'slideshow_body', - ), - FeatureHighlight( - image: 'assets/feature_message/recently_added.webp', - titleKey: 'recently_added_title', - bodyKey: 'recently_added_body', - ), - - FeatureHighlight(image: 'assets/feature_message/ocr.webp', titleKey: 'ocr_title', bodyKey: 'ocr_body'), - FeatureHighlight( - image: 'assets/feature_message/open_in_immich.webp', - titleKey: 'open_in_immich_title', - bodyKey: 'open_in_immich_body', - platform: [.android], - ), - FeatureHighlight(titleKey: 'upload_to_album_title', bodyKey: 'upload_to_album_body'), -]; + FeatureHighlight.values.where((h) => h.isVisibleOnCurrentPlatform).toList(); diff --git a/mobile/lib/domain/models/log.model.dart b/mobile/lib/domain/models/log.model.dart index bed1729f9d..72267bcb5c 100644 --- a/mobile/lib/domain/models/log.model.dart +++ b/mobile/lib/domain/models/log.model.dart @@ -1,51 +1,18 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'log.model.freezed.dart'; + /// Log levels according to dart logging [Level] enum LogLevel { all, finest, finer, fine, config, info, warning, severe, shout, off } -class LogMessage { - final String message; - final LogLevel level; - final DateTime createdAt; - final String? logger; - final String? error; - final String? stack; - - const LogMessage({ - required this.message, - required this.level, - required this.createdAt, - this.logger, - this.error, - this.stack, - }); - - @override - bool operator ==(covariant LogMessage other) { - if (identical(this, other)) { - return true; - } - - return other.message == message && - other.level == level && - other.createdAt == createdAt && - other.logger == logger && - other.error == error && - other.stack == stack; - } - - @override - int get hashCode { - return message.hashCode ^ level.hashCode ^ createdAt.hashCode ^ logger.hashCode ^ error.hashCode ^ stack.hashCode; - } - - @override - String toString() { - return '''LogMessage: { -message: $message, -level: $level, -createdAt: $createdAt, -logger: ${logger ?? ''}, -error: ${error ?? ''}, -stack: ${stack ?? ''}, -}'''; - } +@freezed +abstract class LogMessage with _$LogMessage { + const factory LogMessage({ + required String message, + required LogLevel level, + required DateTime createdAt, + String? logger, + String? error, + String? stack, + }) = _LogMessage; } diff --git a/mobile/lib/domain/models/map.model.dart b/mobile/lib/domain/models/map.model.dart index b55f176bfd..52f6fe8afd 100644 --- a/mobile/lib/domain/models/map.model.dart +++ b/mobile/lib/domain/models/map.model.dart @@ -1,20 +1,9 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; -class Marker { - final LatLng location; - final String assetId; +part 'map.model.freezed.dart'; - const Marker({required this.location, required this.assetId}); - - @override - bool operator ==(covariant Marker other) { - if (identical(this, other)) { - return true; - } - - return other.location == location && other.assetId == assetId; - } - - @override - int get hashCode => location.hashCode ^ assetId.hashCode; +@freezed +abstract class Marker with _$Marker { + const factory Marker({required LatLng location, required String assetId}) = _Marker; } diff --git a/mobile/lib/domain/models/memory.model.dart b/mobile/lib/domain/models/memory.model.dart index e786ca18b1..a75a4a4f0f 100644 --- a/mobile/lib/domain/models/memory.model.dart +++ b/mobile/lib/domain/models/memory.model.dart @@ -1,22 +1,22 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; -import 'package:collection/collection.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +part 'memory.model.freezed.dart'; + +// TODO(agg23): Remove enum suffix enum MemoryTypeEnum { // do not change this order! onThisDay, } -class MemoryData { - final int year; +@Freezed(fromJson: false, toJson: false) +abstract class MemoryData with _$MemoryData { + const MemoryData._(); - const MemoryData({required this.year}); - - MemoryData copyWith({int? year}) { - return MemoryData(year: year ?? this.year); - } + const factory MemoryData({required int year}) = _MemoryData; Map toMap() { return {'year': year}; @@ -29,144 +29,25 @@ class MemoryData { String toJson() => json.encode(toMap()); factory MemoryData.fromJson(String source) => MemoryData.fromMap(json.decode(source) as Map); - - @override - String toString() => 'MemoryData(year: $year)'; - - @override - bool operator ==(covariant MemoryData other) { - if (identical(this, other)) { - return true; - } - - return other.year == year; - } - - @override - int get hashCode => year.hashCode; } // Model for a memory stored in the server -class DriftMemory { - final String id; - final DateTime createdAt; - final DateTime updatedAt; - final DateTime? deletedAt; - final String ownerId; - - // enum - final MemoryTypeEnum type; - final MemoryData data; - final bool isSaved; - final DateTime memoryAt; - final DateTime? seenAt; - final DateTime? showAt; - final DateTime? hideAt; - final List assets; - - const DriftMemory({ - required this.id, - required this.createdAt, - required this.updatedAt, - this.deletedAt, - required this.ownerId, - required this.type, - required this.data, - required this.isSaved, - required this.memoryAt, - this.seenAt, - this.showAt, - this.hideAt, - required this.assets, - }); - - DriftMemory copyWith({ - String? id, - DateTime? createdAt, - DateTime? updatedAt, +// TODO(agg23): DriftMemoryRepository currently mutates `assets` +@Freezed(makeCollectionsUnmodifiable: false) +abstract class DriftMemory with _$DriftMemory { + const factory DriftMemory({ + required String id, + required DateTime createdAt, + required DateTime updatedAt, DateTime? deletedAt, - String? ownerId, - MemoryTypeEnum? type, - MemoryData? data, - bool? isSaved, - DateTime? memoryAt, + required String ownerId, + required MemoryTypeEnum type, + required MemoryData data, + required bool isSaved, + required DateTime memoryAt, DateTime? seenAt, DateTime? showAt, DateTime? hideAt, - List? assets, - }) { - return DriftMemory( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - deletedAt: deletedAt ?? this.deletedAt, - ownerId: ownerId ?? this.ownerId, - type: type ?? this.type, - data: data ?? this.data, - isSaved: isSaved ?? this.isSaved, - memoryAt: memoryAt ?? this.memoryAt, - seenAt: seenAt ?? this.seenAt, - showAt: showAt ?? this.showAt, - hideAt: hideAt ?? this.hideAt, - assets: assets ?? this.assets, - ); - } - - @override - String toString() { - return '''Memory { - id: $id, - createdAt: $createdAt, - updatedAt: $updatedAt, - deletedAt: ${deletedAt ?? ""}, - ownerId: $ownerId, - type: $type, - data: $data, - isSaved: $isSaved, - memoryAt: $memoryAt, - seenAt: ${seenAt ?? ""}, - showAt: ${showAt ?? ""}, - hideAt: ${hideAt ?? ""}, - assets: $assets -}'''; - } - - @override - bool operator ==(covariant DriftMemory other) { - if (identical(this, other)) { - return true; - } - final listEquals = const DeepCollectionEquality().equals; - - return other.id == id && - other.createdAt == createdAt && - other.updatedAt == updatedAt && - other.deletedAt == deletedAt && - other.ownerId == ownerId && - other.type == type && - other.data == data && - other.isSaved == isSaved && - other.memoryAt == memoryAt && - other.seenAt == seenAt && - other.showAt == showAt && - other.hideAt == hideAt && - listEquals(other.assets, assets); - } - - @override - int get hashCode { - return id.hashCode ^ - createdAt.hashCode ^ - updatedAt.hashCode ^ - deletedAt.hashCode ^ - ownerId.hashCode ^ - type.hashCode ^ - data.hashCode ^ - isSaved.hashCode ^ - memoryAt.hashCode ^ - seenAt.hashCode ^ - showAt.hashCode ^ - hideAt.hashCode ^ - assets.hashCode; - } + required List assets, + }) = _DriftMemory; } diff --git a/mobile/lib/domain/models/person.model.dart b/mobile/lib/domain/models/person.model.dart index 38d740eeb4..94ead6a2e9 100644 --- a/mobile/lib/domain/models/person.model.dart +++ b/mobile/lib/domain/models/person.model.dart @@ -1,115 +1,9 @@ -import 'dart:convert'; - import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'person.model.freezed.dart'; -// TODO: Remove PersonDto once Isar is removed -class PersonDto { - const PersonDto({ - required this.id, - this.birthDate, - required this.isHidden, - required this.name, - required this.thumbnailPath, - this.updatedAt, - }); - - final String id; - final DateTime? birthDate; - final bool isHidden; - final String name; - final String thumbnailPath; - final DateTime? updatedAt; - - @override - String toString() { - return 'Person(id: $id, birthDate: $birthDate, isHidden: $isHidden, name: $name, thumbnailPath: $thumbnailPath, updatedAt: $updatedAt)'; - } - - PersonDto copyWith({ - String? id, - DateTime? birthDate, - bool? isHidden, - String? name, - String? thumbnailPath, - DateTime? updatedAt, - }) { - return PersonDto( - id: id ?? this.id, - birthDate: birthDate ?? this.birthDate, - isHidden: isHidden ?? this.isHidden, - name: name ?? this.name, - thumbnailPath: thumbnailPath ?? this.thumbnailPath, - updatedAt: updatedAt ?? this.updatedAt, - ); - } - - Map toMap() { - return { - 'id': id, - 'birthDate': birthDate?.millisecondsSinceEpoch, - 'isHidden': isHidden, - 'name': name, - 'thumbnailPath': thumbnailPath, - 'updatedAt': updatedAt?.millisecondsSinceEpoch, - }; - } - - factory PersonDto.fromMap(Map map) { - return PersonDto( - id: map['id'] as String, - birthDate: map['birthDate'] != null ? DateTime.fromMillisecondsSinceEpoch(map['birthDate'] as int) : null, - isHidden: map['isHidden'] as bool, - name: map['name'] as String, - thumbnailPath: map['thumbnailPath'] as String, - updatedAt: map['updatedAt'] != null ? DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int) : null, - ); - } - - String toJson() => json.encode(toMap()); - - factory PersonDto.fromJson(String source) => PersonDto.fromMap(json.decode(source) as Map); - - @override - bool operator ==(covariant PersonDto other) { - if (identical(this, other)) { - return true; - } - - return other.id == id && - other.birthDate == birthDate && - other.isHidden == isHidden && - other.name == name && - other.thumbnailPath == thumbnailPath && - other.updatedAt == updatedAt; - } - - @override - int get hashCode { - return id.hashCode ^ - birthDate.hashCode ^ - isHidden.hashCode ^ - name.hashCode ^ - thumbnailPath.hashCode ^ - updatedAt.hashCode; - } -} - -// Model for a person stored in the server @freezed -abstract class DriftPerson with _$DriftPerson { - const factory DriftPerson({ - required String id, - required DateTime createdAt, - required DateTime updatedAt, - required String ownerId, - required String name, - String? faceAssetId, - required bool isFavorite, - required bool isHidden, - required String? color, - DateTime? birthDate, - }) = _DriftPerson; +abstract class Person with _$Person { + const factory Person({required String id, required String name, DateTime? updatedAt, DateTime? birthDate}) = _Person; } diff --git a/mobile/lib/domain/models/search_result.model.dart b/mobile/lib/domain/models/search_result.model.dart index 6a782e2f37..11409e64a2 100644 --- a/mobile/lib/domain/models/search_result.model.dart +++ b/mobile/lib/domain/models/search_result.model.dart @@ -1,25 +1,15 @@ -import 'package:collection/collection.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -class SearchResult { - final List assets; - final int? nextPage; +part 'search_result.model.freezed.dart'; - const SearchResult({required this.assets, this.nextPage}); +@Freezed(toStringOverride: false) +abstract class SearchResult with _$SearchResult { + const SearchResult._(); + const factory SearchResult({required List assets, int? nextPage}) = _SearchResult; + + // Explicitly don't log results, only attributes @override String toString() => 'SearchResult(assets: ${assets.length}, nextPage: $nextPage)'; - - @override - bool operator ==(covariant SearchResult other) { - if (identical(this, other)) { - return true; - } - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.assets, assets) && other.nextPage == nextPage; - } - - @override - int get hashCode => assets.hashCode ^ nextPage.hashCode; } diff --git a/mobile/lib/domain/models/tag.model.dart b/mobile/lib/domain/models/tag.model.dart index ba9aef02ee..d38931a285 100644 --- a/mobile/lib/domain/models/tag.model.dart +++ b/mobile/lib/domain/models/tag.model.dart @@ -1,29 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:openapi/api.dart'; -class Tag { - final String id; - final String value; +part 'tag.model.freezed.dart'; - const Tag({required this.id, required this.value}); - - @override - String toString() { - return 'Tag(id: $id, value: $value)'; - } - - @override - bool operator ==(covariant Tag other) { - if (identical(this, other)) { - return true; - } - - return other.id == id && other.value == value; - } - - @override - int get hashCode { - return id.hashCode ^ value.hashCode; - } +@freezed +abstract class Tag with _$Tag { + const factory Tag({required String id, required String value}) = _Tag; static Tag fromDto(TagResponseDto dto) { return Tag(id: dto.id, value: dto.value); diff --git a/mobile/lib/domain/models/user.model.dart b/mobile/lib/domain/models/user.model.dart index d1a7fc5546..96e07137c9 100644 --- a/mobile/lib/domain/models/user.model.dart +++ b/mobile/lib/domain/models/user.model.dart @@ -1,7 +1,10 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; import 'dart:ui'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user.model.freezed.dart'; + enum AvatarColor { // do not change this order or reuse indices for other purposes, adding is OK primary("primary"), @@ -164,78 +167,15 @@ profileChangedAt: $profileChangedAt quotaUsageInBytes.hashCode; } -class PartnerUserDto { - final String id; - final String email; - final String name; - final bool inTimeline; - - final String? profileImagePath; - - const PartnerUserDto({ - required this.id, - required this.email, - required this.name, - required this.inTimeline, - this.profileImagePath, - }); - - PartnerUserDto copyWith({String? id, String? email, String? name, bool? inTimeline, String? profileImagePath}) { - return PartnerUserDto( - id: id ?? this.id, - email: email ?? this.email, - name: name ?? this.name, - inTimeline: inTimeline ?? this.inTimeline, - profileImagePath: profileImagePath ?? this.profileImagePath, - ); - } - - Map toMap() { - return { - 'id': id, - 'email': email, - 'name': name, - 'inTimeline': inTimeline, - 'profileImagePath': profileImagePath, - }; - } - - factory PartnerUserDto.fromMap(Map map) { - return PartnerUserDto( - id: map['id'] as String, - email: map['email'] as String, - name: map['name'] as String, - inTimeline: map['inTimeline'] as bool, - profileImagePath: map['profileImagePath'] != null ? map['profileImagePath'] as String : null, - ); - } - - String toJson() => json.encode(toMap()); - - factory PartnerUserDto.fromJson(String source) => PartnerUserDto.fromMap(json.decode(source) as Map); - - @override - String toString() { - return 'PartnerUserDto(id: $id, email: $email, name: $name, inTimeline: $inTimeline, profileImagePath: $profileImagePath)'; - } - - @override - bool operator ==(covariant PartnerUserDto other) { - if (identical(this, other)) { - return true; - } - - return other.id == id && - other.email == email && - other.name == name && - other.inTimeline == inTimeline && - other.profileImagePath == profileImagePath; - } - - @override - int get hashCode { - return id.hashCode ^ email.hashCode ^ name.hashCode ^ inTimeline.hashCode ^ profileImagePath.hashCode; - } +@freezed +abstract class PartnerUserDto with _$PartnerUserDto { + const factory PartnerUserDto({ + required String id, + required String email, + required String name, + required bool inTimeline, + String? profileImagePath, + }) = _PartnerUserDto; } class User { diff --git a/mobile/lib/domain/models/user_metadata.model.dart b/mobile/lib/domain/models/user_metadata.model.dart index 8bce1aca13..2b1da0a809 100644 --- a/mobile/lib/domain/models/user_metadata.model.dart +++ b/mobile/lib/domain/models/user_metadata.model.dart @@ -11,105 +11,32 @@ enum UserMetadataKey { license, } -class Onboarding { - final bool isOnboarded; +@freezed +abstract class Onboarding with _$Onboarding { + const Onboarding._(); - const Onboarding({required this.isOnboarded}); - - Onboarding copyWith({bool? isOnboarded}) { - return Onboarding(isOnboarded: isOnboarded ?? this.isOnboarded); - } - - Map toMap() { - final onboarding = {}; - onboarding["isOnboarded"] = isOnboarded; - return onboarding; - } + const factory Onboarding({required bool isOnboarded}) = _Onboarding; factory Onboarding.fromMap(Map map) { return Onboarding(isOnboarded: map["isOnboarded"]! as bool); } - - @override - String toString() { - return '''Onboarding { -isOnboarded: $isOnboarded, -}'''; - } - - @override - bool operator ==(covariant Onboarding other) { - if (identical(this, other)) { - return true; - } - - return isOnboarded == other.isOnboarded; - } - - @override - int get hashCode => isOnboarded.hashCode; } -class Preferences { - final bool foldersEnabled; - final bool memoriesEnabled; - final bool peopleEnabled; - final bool ratingsEnabled; - final bool sharedLinksEnabled; - final bool tagsEnabled; - final AvatarColor userAvatarColor; - final bool showSupportBadge; - final int minimumFaces; +@freezed +abstract class Preferences with _$Preferences { + const Preferences._(); - const Preferences({ - this.foldersEnabled = false, - this.memoriesEnabled = true, - this.peopleEnabled = true, - this.ratingsEnabled = false, - this.sharedLinksEnabled = true, - this.tagsEnabled = false, - this.userAvatarColor = AvatarColor.primary, - this.showSupportBadge = true, - this.minimumFaces = 3, - }); - - Preferences copyWith({ - bool? foldersEnabled, - bool? memoriesEnabled, - bool? peopleEnabled, - bool? ratingsEnabled, - bool? sharedLinksEnabled, - bool? tagsEnabled, - AvatarColor? userAvatarColor, - bool? showSupportBadge, - int? minimumFaces, - }) { - return Preferences( - foldersEnabled: foldersEnabled ?? this.foldersEnabled, - memoriesEnabled: memoriesEnabled ?? this.memoriesEnabled, - peopleEnabled: peopleEnabled ?? this.peopleEnabled, - ratingsEnabled: ratingsEnabled ?? this.ratingsEnabled, - sharedLinksEnabled: sharedLinksEnabled ?? this.sharedLinksEnabled, - tagsEnabled: tagsEnabled ?? this.tagsEnabled, - userAvatarColor: userAvatarColor ?? this.userAvatarColor, - showSupportBadge: showSupportBadge ?? this.showSupportBadge, - minimumFaces: minimumFaces ?? this.minimumFaces, - ); - } - - Map toMap() { - final preferences = {}; - preferences["folders-Enabled"] = foldersEnabled; - preferences["memories-Enabled"] = memoriesEnabled; - preferences["people-Enabled"] = peopleEnabled; - preferences["ratings-Enabled"] = ratingsEnabled; - preferences["sharedLinks-Enabled"] = sharedLinksEnabled; - preferences["tags-Enabled"] = tagsEnabled; - preferences["avatar-Color"] = userAvatarColor.value; - preferences["purchase-ShowSupportBadge"] = showSupportBadge; - preferences["minimumFaces"] = minimumFaces; - return preferences; - } + const factory Preferences({ + @Default(false) bool foldersEnabled, + @Default(true) bool memoriesEnabled, + @Default(true) bool peopleEnabled, + @Default(false) bool ratingsEnabled, + @Default(true) bool sharedLinksEnabled, + @Default(false) bool tagsEnabled, + @Default(AvatarColor.primary) AvatarColor userAvatarColor, + @Default(true) bool showSupportBadge, + @Default(3) int minimumFaces, + }) = _Preferences; factory Preferences.fromMap(Map map) { return Preferences( @@ -127,75 +54,14 @@ class Preferences { minimumFaces: (map["people"] as Map?)?["minimumFaces"] as int? ?? 3, ); } - - @override - String toString() { - return '''Preferences: { -foldersEnabled: $foldersEnabled, -memoriesEnabled: $memoriesEnabled, -peopleEnabled: $peopleEnabled, -ratingsEnabled: $ratingsEnabled, -sharedLinksEnabled: $sharedLinksEnabled, -tagsEnabled: $tagsEnabled, -userAvatarColor: $userAvatarColor, -showSupportBadge: $showSupportBadge, -minimumFaces: $minimumFaces, -}'''; - } - - @override - bool operator ==(covariant Preferences other) { - if (identical(this, other)) { - return true; - } - - return other.foldersEnabled == foldersEnabled && - other.memoriesEnabled == memoriesEnabled && - other.peopleEnabled == peopleEnabled && - other.ratingsEnabled == ratingsEnabled && - other.sharedLinksEnabled == sharedLinksEnabled && - other.tagsEnabled == tagsEnabled && - other.userAvatarColor == userAvatarColor && - other.showSupportBadge == showSupportBadge && - other.minimumFaces == minimumFaces; - } - - @override - int get hashCode { - return foldersEnabled.hashCode ^ - memoriesEnabled.hashCode ^ - peopleEnabled.hashCode ^ - ratingsEnabled.hashCode ^ - sharedLinksEnabled.hashCode ^ - tagsEnabled.hashCode ^ - userAvatarColor.hashCode ^ - showSupportBadge.hashCode ^ - minimumFaces.hashCode; - } } -class License { - final DateTime activatedAt; - final String activationKey; - final String licenseKey; +@freezed +abstract class License with _$License { + const License._(); - const License({required this.activatedAt, required this.activationKey, required this.licenseKey}); - - License copyWith({DateTime? activatedAt, String? activationKey, String? licenseKey}) { - return License( - activatedAt: activatedAt ?? this.activatedAt, - activationKey: activationKey ?? this.activationKey, - licenseKey: licenseKey ?? this.licenseKey, - ); - } - - Map toMap() { - final license = {}; - license["activatedAt"] = activatedAt; - license["activationKey"] = activationKey; - license["licenseKey"] = licenseKey; - return license; - } + const factory License({required DateTime activatedAt, required String activationKey, required String licenseKey}) = + _License; factory License.fromMap(Map map) { return License( @@ -204,27 +70,6 @@ class License { licenseKey: map["licenseKey"]! as String, ); } - - @override - String toString() { - return '''License { -activatedAt: $activatedAt, -activationKey: $activationKey, -licenseKey: $licenseKey, -}'''; - } - - @override - bool operator ==(covariant License other) { - if (identical(this, other)) { - return true; - } - - return activatedAt == other.activatedAt && activationKey == other.activationKey && licenseKey == other.licenseKey; - } - - @override - int get hashCode => activatedAt.hashCode ^ activationKey.hashCode ^ licenseKey.hashCode; } // Model for a user metadata stored in the server diff --git a/mobile/lib/domain/services/feature_message.service.dart b/mobile/lib/domain/services/feature_message.service.dart index ff0fdda8f3..5eb58f0823 100644 --- a/mobile/lib/domain/services/feature_message.service.dart +++ b/mobile/lib/domain/services/feature_message.service.dart @@ -9,7 +9,7 @@ class FeatureMessageService { bool shouldShow() { final seen = _settingsRepository.appConfig.featureMessage.seenRelease; - return featureMessageHighlights.isNotEmpty && featureMessageRelease > seen; + return visibleFeatureMessageHighlights.isNotEmpty && featureMessageRelease > seen; } Future markSeen() => _settingsRepository.write(SettingsKey.featureMessageSeenRelease, featureMessageRelease); diff --git a/mobile/lib/domain/services/people.service.dart b/mobile/lib/domain/services/people.service.dart index 73972dbdc0..196cf1edbb 100644 --- a/mobile/lib/domain/services/people.service.dart +++ b/mobile/lib/domain/services/people.service.dart @@ -10,16 +10,16 @@ class DriftPeopleService { const DriftPeopleService(this._repository, this._personApiRepository); - Future get(String personId) { + Future get(String personId) { return _repository.get(personId); } - Future> getAssetPeople(String assetId) { + Future> getAssetPeople(String assetId) { return _repository.getAssetPeople(assetId); } - Future> getAllPeople({int minFaces = 3}) { - return _repository.getAllPeople(minFaces: minFaces); + Stream> watch({int minFaces = 3}) { + return _repository.watch(minFaces: minFaces); } Future updateName(String personId, String name) async { @@ -27,7 +27,7 @@ class DriftPeopleService { return _repository.updateName(personId, name); } - Future updateBrithday(String personId, DateTime birthday) async { + Future updateBirthday(String personId, DateTime birthday) async { await _personApiRepository.update(personId, birthday: birthday); return _repository.updateBirthday(personId, birthday); } diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index db0d93c92f..f1e0328165 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -50,16 +50,39 @@ class BackgroundSyncManager { this.onCloudIdSyncError, }); + // The tasks the app-resume path re-runs. One in-flight when the app was suspended + // stays referenced but frozen, so on resume the dedupe guards would hand back the + // stale task instead of syncing (#28082). Websocket and cloud-id are excluded - the + // resume path never restarts them. [_allTasks] builds on this so the lists can't drift. + List get _resumeSyncTasks => [_syncTask, _deviceAlbumSyncTask, _hashTask, _linkedAlbumSyncTask]; + + List get _allTasks => [_syncWebsocketTask, _cloudIdSyncTask, ..._resumeSyncTasks]; + Future cancel() async { _syncQueued = false; - final tasks = [ - _syncTask, - _syncWebsocketTask, - _cloudIdSyncTask, - _linkedAlbumSyncTask, - _deviceAlbumSyncTask, - _hashTask, - ]; + final tasks = _allTasks; + _syncTask = null; + _syncWebsocketTask = null; + _cloudIdSyncTask = null; + _linkedAlbumSyncTask = null; + _deviceAlbumSyncTask = null; + _hashTask = null; + await _cancelAll(tasks); + } + + Future cancelResumeSyncs() async { + _syncQueued = false; + final tasks = _resumeSyncTasks; + _syncTask = null; + _deviceAlbumSyncTask = null; + _hashTask = null; + _linkedAlbumSyncTask = null; + await _cancelAll(tasks); + } + + // Cancels every task in [tasks] and waits for them to unwind. Callers null out + // their own fields first, so the sync guards see a clean slate immediately. + Future _cancelAll(List tasks) async { final futures = [ for (final task in tasks) if (task != null) task.future, @@ -67,13 +90,6 @@ class BackgroundSyncManager { for (final task in tasks) { task?.cancel(); } - _syncTask = null; - _syncWebsocketTask = null; - _cloudIdSyncTask = null; - _linkedAlbumSyncTask = null; - _deviceAlbumSyncTask = null; - _hashTask = null; - try { await Future.wait(futures); } on CanceledError { @@ -84,14 +100,14 @@ class BackgroundSyncManager { // No need to cancel the task, as it can also be run when the user logs out Future syncLocal({bool full = false}) { if (_deviceAlbumSyncTask != null) { - return _deviceAlbumSyncTask!.future; + return _deviceAlbumSyncTask!.future.catchError((_) {}, test: (error) => error is CanceledError); } onLocalSyncStart?.call(); // We use a ternary operator to avoid [_deviceAlbumSyncTask] from being // captured by the closure passed to [runInIsolateGentle]. - _deviceAlbumSyncTask = full + final task = _deviceAlbumSyncTask = full ? runInIsolateGentle( computation: (ref) => ref.read(localSyncServiceProvider).sync(full: true), debugLabel: 'local-sync-full-true', @@ -101,37 +117,43 @@ class BackgroundSyncManager { debugLabel: 'local-sync-full-false', ); - return _deviceAlbumSyncTask! + return task .whenComplete(() { - _deviceAlbumSyncTask = null; + if (identical(_deviceAlbumSyncTask, task)) { + _deviceAlbumSyncTask = null; + } onLocalSyncComplete?.call(); }) .catchError((error) { - onLocalSyncError?.call(error.toString()); - _deviceAlbumSyncTask = null; + if (error is! CanceledError) { + onLocalSyncError?.call(error.toString()); + } }); } Future hashAssets() { if (_hashTask != null) { - return _hashTask!.future; + return _hashTask!.future.catchError((_) {}, test: (error) => error is CanceledError); } onHashingStart?.call(); - _hashTask = runInIsolateGentle( + final task = _hashTask = runInIsolateGentle( computation: (ref) => ref.read(hashServiceProvider).hashAssets(), debugLabel: 'hash-assets', ); - return _hashTask! + return task .whenComplete(() { onHashingComplete?.call(); - _hashTask = null; + if (identical(_hashTask, task)) { + _hashTask = null; + } }) .catchError((error) { - onHashingError?.call(error.toString()); - _hashTask = null; + if (error is! CanceledError) { + onHashingError?.call(error.toString()); + } }); } @@ -143,11 +165,11 @@ class BackgroundSyncManager { onRemoteSyncStart?.call(); - _syncTask = runInIsolateGentle( + final task = _syncTask = runInIsolateGentle( computation: (ref) => ref.read(syncStreamServiceProvider).sync(), debugLabel: 'remote-sync', ); - return _syncTask! + return task .then((result) { final success = result ?? false; onRemoteSyncComplete?.call(success); @@ -155,15 +177,21 @@ class BackgroundSyncManager { return success; }) .catchError((error) { - onRemoteSyncError?.call(error.toString()); + if (error is! CanceledError) { + onRemoteSyncError?.call(error.toString()); + } _syncQueued = false; return false; }) + // A task clears only its own slot: one that was cancelled and superseded by a + // fresh task (see cancelResumeSyncs) must not null the new task's slot. .whenComplete(() { - _syncTask = null; - if (_syncQueued) { - _syncQueued = false; - unawaited(syncRemote()); + if (identical(_syncTask, task)) { + _syncTask = null; + if (_syncQueued) { + _syncQueued = false; + unawaited(syncRemote()); + } } }); } @@ -210,13 +238,21 @@ class BackgroundSyncManager { Future syncLinkedAlbum() { if (_linkedAlbumSyncTask != null) { - return _linkedAlbumSyncTask!.future; + return _linkedAlbumSyncTask!.future.catchError((_) {}, test: (error) => error is CanceledError); } - _linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated, debugLabel: 'linked-album-sync'); - return _linkedAlbumSyncTask!.whenComplete(() { - _linkedAlbumSyncTask = null; - }); + final task = _linkedAlbumSyncTask = runInIsolateGentle( + computation: syncLinkedAlbumsIsolated, + debugLabel: 'linked-album-sync', + ); + return task + .whenComplete(() { + if (identical(_linkedAlbumSyncTask, task)) { + _linkedAlbumSyncTask = null; + } + }) + // a cancelled resume sync is not a failure; absorb it so the websocket callers don't get an uncaught error + .catchError((_) {}, test: (error) => error is CanceledError); } Future syncCloudIds() { diff --git a/mobile/lib/extensions/translate_extensions.dart b/mobile/lib/extensions/translate_extensions.dart deleted file mode 100644 index b01203a90c..0000000000 --- a/mobile/lib/extensions/translate_extensions.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; -import 'package:intl/message_format.dart'; - -extension StringTranslateExtension on String { - String t({BuildContext? context, Map? args}) { - return _translateHelper(context, this, args); - } -} - -extension TextTranslateExtension on Text { - Text t({BuildContext? context, Map? args}) { - return Text( - _translateHelper(context, data ?? '', args), - key: key, - style: style, - strutStyle: strutStyle, - textAlign: textAlign, - textDirection: textDirection, - locale: locale, - softWrap: softWrap, - overflow: overflow, - textScaler: textScaler, - maxLines: maxLines, - semanticsLabel: semanticsLabel, - textWidthBasis: textWidthBasis, - textHeightBehavior: textHeightBehavior, - ); - } -} - -String _translateHelper(BuildContext? context, String key, [Map? args]) { - if (key.isEmpty) { - return ''; - } - try { - final translatedMessage = key.tr(context: context); - return args != null - ? MessageFormat(translatedMessage, locale: Intl.defaultLocale ?? 'en').format(args) - : translatedMessage; - } catch (e) { - dPrint(() => 'Translation failed for key "$key". Error: $e'); - return key; - } -} diff --git a/mobile/lib/infrastructure/loaders/image_request.dart b/mobile/lib/infrastructure/loaders/image_request.dart index 8b7cdc7621..6b2be218dc 100644 --- a/mobile/lib/infrastructure/loaders/image_request.dart +++ b/mobile/lib/infrastructure/loaders/image_request.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:ffi'; +import 'dart:math' as math; import 'dart:ui' as ui; import 'package:ffi/ffi.dart'; @@ -36,7 +37,11 @@ abstract class ImageRequest { void _onCancelled(); - Future<(ui.Codec, ui.ImageDescriptor)?> _codecFromEncodedPlatformImage(int address, int length) async { + Future<(ui.Codec, ui.ImageDescriptor)?> _codecFromEncodedPlatformImage( + int address, + int length, { + ui.Size? decodeSize, + }) async { final pointer = Pointer.fromAddress(address); if (_isCancelled) { malloc.free(pointer); @@ -62,7 +67,8 @@ abstract class ImageRequest { return null; } - final codec = await descriptor.instantiateCodec(); + final target = _targetSize(descriptor.width, descriptor.height, decodeSize); + final codec = await descriptor.instantiateCodec(targetWidth: target?.$1, targetHeight: target?.$2); if (_isCancelled) { descriptor.dispose(); codec.dispose(); @@ -72,8 +78,8 @@ abstract class ImageRequest { return (codec, descriptor); } - Future _fromEncodedPlatformImage(int address, int length) async { - final result = await _codecFromEncodedPlatformImage(address, length); + Future _fromEncodedPlatformImage(int address, int length, {ui.Size? decodeSize}) async { + final result = await _codecFromEncodedPlatformImage(address, length, decodeSize: decodeSize); if (result == null) { return null; } @@ -96,6 +102,19 @@ abstract class ImageRequest { return frame; } + (int, int)? _targetSize(int width, int height, ui.Size? decodeSize) { + if (width <= 0 || height <= 0 || decodeSize == null || decodeSize.width <= 0 || decodeSize.height <= 0) { + return null; + } + + final scale = math.max(decodeSize.width / width, decodeSize.height / height); + if (scale >= 1) { + return null; + } + + return ((width * scale).ceil(), (height * scale).ceil()); + } + Future _fromDecodedPlatformImage(int address, int width, int height, int rowBytes) async { final pointer = Pointer.fromAddress(address); if (_isCancelled) { diff --git a/mobile/lib/infrastructure/loaders/remote_image_request.dart b/mobile/lib/infrastructure/loaders/remote_image_request.dart index d6a25753fb..7b504697ac 100644 --- a/mobile/lib/infrastructure/loaders/remote_image_request.dart +++ b/mobile/lib/infrastructure/loaders/remote_image_request.dart @@ -3,7 +3,10 @@ part of 'image_request.dart'; class RemoteImageRequest extends ImageRequest { final String uri; - RemoteImageRequest({required this.uri}); + /// Physical size to decode, or null for the source size. + final ui.Size? decodeSize; + + RemoteImageRequest({required this.uri, this.decodeSize}); @override Future load(ImageDecoderCallback decode, {double scale = 1.0}) async { @@ -11,10 +14,20 @@ class RemoteImageRequest extends ImageRequest { return null; } - final info = await remoteImageApi.requestImage(uri, requestId: requestId, preferEncoded: false); + final info = await remoteImageApi.requestImage( + uri, + requestId: requestId, + preferEncoded: false, + width: decodeSize?.width.ceil(), + height: decodeSize?.height.ceil(), + ); // Android falls back to encoded data if native decoding fails, so check for both shapes of the response. final frame = switch (info) { - {'pointer': final int pointer, 'length': final int length} => await _fromEncodedPlatformImage(pointer, length), + {'pointer': final int pointer, 'length': final int length} => await _fromEncodedPlatformImage( + pointer, + length, + decodeSize: decodeSize, + ), { 'pointer': final int pointer, 'width': final int width, @@ -33,7 +46,13 @@ class RemoteImageRequest extends ImageRequest { return null; } - final info = await remoteImageApi.requestImage(uri, requestId: requestId, preferEncoded: true); + final info = await remoteImageApi.requestImage( + uri, + requestId: requestId, + preferEncoded: true, + width: null, + height: null, + ); if (info == null) { return null; } diff --git a/mobile/lib/infrastructure/repositories/people.repository.dart b/mobile/lib/infrastructure/repositories/people.repository.dart index 0dd73cc81d..c5c7bfdb9c 100644 --- a/mobile/lib/infrastructure/repositories/people.repository.dart +++ b/mobile/lib/infrastructure/repositories/people.repository.dart @@ -8,14 +8,14 @@ class DriftPeopleRepository extends DriftDatabaseRepository { final Drift _db; const DriftPeopleRepository(this._db) : super(_db); - Future get(String personId) async { + Future get(String personId) async { final query = _db.select(_db.personEntity)..where((row) => row.id.equals(personId)); final result = await query.getSingleOrNull(); return result?.toDto(); } - Future> getAssetPeople(String assetId) async { + Future> getAssetPeople(String assetId) async { // An asset can have multiple face records for the same person (e.g., metadata // imports alongside ML detections). Use a subquery instead of a join so each // person is returned once, regardless of how many of their faces are on the asset @@ -33,7 +33,7 @@ class DriftPeopleRepository extends DriftDatabaseRepository { return query.map((row) => row.toDto()).get(); } - Future> getAllPeople({int minFaces = 3}) async { + Stream> watch({int minFaces = 3}) { final people = _db.personEntity; final faces = _db.assetFaceEntity; final assets = _db.remoteAssetEntity; @@ -59,7 +59,7 @@ class DriftPeopleRepository extends DriftDatabaseRepository { return query.map((row) { final person = row.readTable(people); return person.toDto(); - }).get(); + }).watch(); } Future updateName(String personId, String name) { @@ -76,18 +76,5 @@ class DriftPeopleRepository extends DriftDatabaseRepository { } extension on PersonEntityData { - DriftPerson toDto() { - return DriftPerson( - id: id, - createdAt: createdAt, - updatedAt: updatedAt, - ownerId: ownerId, - name: name, - faceAssetId: faceAssetId, - isFavorite: isFavorite, - isHidden: isHidden, - color: color, - birthDate: birthDate, - ); - } + Person toDto() => Person(id: id, updatedAt: updatedAt, name: name, birthDate: birthDate); } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 317733f1de..2f1cfdaf6a 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -16,7 +16,6 @@ import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/domain/services/background_worker.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; @@ -273,15 +272,12 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve darkTheme: getThemeData(colorScheme: immichTheme.dark, locale: context.locale), theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale), builder: (context, child) => ImmichTranslationProvider( - translations: ImmichTranslations( - submit: "submit".t(context: context), - password: "password".t(context: context), - ), + translations: ImmichTranslations(submit: context.t.submit, password: context.t.password), child: ImmichThemeProvider(colorScheme: context.colorScheme, child: child!), ), routerConfig: router.config( deepLinkBuilder: _deepLinkBuilder, - navigatorObservers: () => [AppNavigationObserver(ref: ref)], + navigatorObservers: () => [AppNavigationObserver(ref: ref), TransitioningRouteObserver()], ), ), ); diff --git a/mobile/lib/models/auth/auxilary_endpoint.model.dart b/mobile/lib/models/auth/auxilary_endpoint.model.dart index cd11918bed..8686ae9461 100644 --- a/mobile/lib/models/auth/auxilary_endpoint.model.dart +++ b/mobile/lib/models/auth/auxilary_endpoint.model.dart @@ -1,34 +1,14 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; -class AuxilaryEndpoint { - final String url; - final AuxCheckStatus status; +import 'package:freezed_annotation/freezed_annotation.dart'; - const AuxilaryEndpoint({required this.url, required this.status}); +part 'auxilary_endpoint.model.freezed.dart'; - AuxilaryEndpoint copyWith({String? url, AuxCheckStatus? status}) { - return AuxilaryEndpoint(url: url ?? this.url, status: status ?? this.status); - } +@Freezed(fromJson: false, toJson: false) +abstract class AuxilaryEndpoint with _$AuxilaryEndpoint { + const AuxilaryEndpoint._(); - @override - String toString() => 'AuxilaryEndpoint(url: $url, status: $status)'; - - @override - bool operator ==(covariant AuxilaryEndpoint other) { - if (identical(this, other)) { - return true; - } - - return other.url == url && other.status == status; - } - - @override - int get hashCode => url.hashCode ^ status.hashCode; - - Map toMap() { - return {'url': url, 'status': status.toMap()}; - } + const factory AuxilaryEndpoint({required String url, required AuxCheckStatus status}) = _AuxilaryEndpoint; factory AuxilaryEndpoint.fromMap(Map map) { return AuxilaryEndpoint( @@ -37,50 +17,23 @@ class AuxilaryEndpoint { ); } - String toJson() => json.encode(toMap()); - factory AuxilaryEndpoint.fromJson(String source) => AuxilaryEndpoint.fromMap(json.decode(source) as Map); } -class AuxCheckStatus { - final String name; - const AuxCheckStatus({required this.name}); - const AuxCheckStatus._(this.name); +// TODO(agg23): Should be an enum +@freezed +abstract class AuxCheckStatus with _$AuxCheckStatus { + const AuxCheckStatus._(); - static const loading = AuxCheckStatus._('loading'); - static const valid = AuxCheckStatus._('valid'); - static const error = AuxCheckStatus._('error'); - static const unknown = AuxCheckStatus._('unknown'); + const factory AuxCheckStatus({required String name}) = _AuxCheckStatus; - @override - bool operator ==(covariant AuxCheckStatus other) { - if (identical(this, other)) { - return true; - } - - return other.name == name; - } - - @override - int get hashCode => name.hashCode; - - AuxCheckStatus copyWith({String? name}) { - return AuxCheckStatus(name: name ?? this.name); - } - - Map toMap() { - return {'name': name}; - } + static const loading = AuxCheckStatus(name: 'loading'); + static const valid = AuxCheckStatus(name: 'valid'); + static const error = AuxCheckStatus(name: 'error'); + static const unknown = AuxCheckStatus(name: 'unknown'); factory AuxCheckStatus.fromMap(Map map) { return AuxCheckStatus(name: map['name'] as String); } - - String toJson() => json.encode(toMap()); - - factory AuxCheckStatus.fromJson(String source) => AuxCheckStatus.fromMap(json.decode(source) as Map); - - @override - String toString() => 'AuxCheckStatus(name: $name)'; } diff --git a/mobile/lib/models/auth/biometric_status.model.dart b/mobile/lib/models/auth/biometric_status.model.dart index c5c417d06d..691fee3468 100644 --- a/mobile/lib/models/auth/biometric_status.model.dart +++ b/mobile/lib/models/auth/biometric_status.model.dart @@ -1,32 +1,10 @@ -import 'package:collection/collection.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:local_auth/local_auth.dart'; -class BiometricStatus { - final List availableBiometrics; - final bool canAuthenticate; +part 'biometric_status.model.freezed.dart'; - const BiometricStatus({required this.availableBiometrics, required this.canAuthenticate}); - - @override - String toString() => 'BiometricStatus(availableBiometrics: $availableBiometrics, canAuthenticate: $canAuthenticate)'; - - BiometricStatus copyWith({List? availableBiometrics, bool? canAuthenticate}) { - return BiometricStatus( - availableBiometrics: availableBiometrics ?? this.availableBiometrics, - canAuthenticate: canAuthenticate ?? this.canAuthenticate, - ); - } - - @override - bool operator ==(covariant BiometricStatus other) { - if (identical(this, other)) { - return true; - } - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.availableBiometrics, availableBiometrics) && other.canAuthenticate == canAuthenticate; - } - - @override - int get hashCode => availableBiometrics.hashCode ^ canAuthenticate.hashCode; +@freezed +abstract class BiometricStatus with _$BiometricStatus { + const factory BiometricStatus({required List availableBiometrics, required bool canAuthenticate}) = + _BiometricStatus; } diff --git a/mobile/lib/models/cast/cast_manager_state.dart b/mobile/lib/models/cast/cast_manager_state.dart index 9727bc7ed8..0fd3dc877b 100644 --- a/mobile/lib/models/cast/cast_manager_state.dart +++ b/mobile/lib/models/cast/cast_manager_state.dart @@ -1,85 +1,18 @@ -import 'dart:convert'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'cast_manager_state.freezed.dart'; enum CastDestinationType { googleCast } enum CastState { idle, playing, paused, buffering } -class CastManagerState { - final bool isCasting; - final String receiverName; - final CastState castState; - final Duration currentTime; - final Duration duration; - - const CastManagerState({ - required this.isCasting, - required this.receiverName, - required this.castState, - required this.currentTime, - required this.duration, - }); - - CastManagerState copyWith({ - bool? isCasting, - String? receiverName, - CastState? castState, - Duration? currentTime, - Duration? duration, - }) { - return CastManagerState( - isCasting: isCasting ?? this.isCasting, - receiverName: receiverName ?? this.receiverName, - castState: castState ?? this.castState, - currentTime: currentTime ?? this.currentTime, - duration: duration ?? this.duration, - ); - } - - Map toMap() { - final result = {}; - - result.addAll({'isCasting': isCasting}); - result.addAll({'receiverName': receiverName}); - result.addAll({'castState': castState}); - result.addAll({'currentTime': currentTime.inSeconds}); - result.addAll({'duration': duration.inSeconds}); - - return result; - } - - factory CastManagerState.fromMap(Map map) { - return CastManagerState( - isCasting: map['isCasting'] ?? false, - receiverName: map['receiverName'] ?? '', - castState: map['castState'] ?? CastState.idle, - currentTime: Duration(seconds: map['currentTime']?.toInt() ?? 0), - duration: Duration(seconds: map['duration']?.toInt() ?? 0), - ); - } - - String toJson() => json.encode(toMap()); - - factory CastManagerState.fromJson(String source) => CastManagerState.fromMap(json.decode(source)); - - @override - String toString() => - 'CastManagerState(isCasting: $isCasting, receiverName: $receiverName, castState: $castState, currentTime: $currentTime, duration: $duration)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is CastManagerState && - other.isCasting == isCasting && - other.receiverName == receiverName && - other.castState == castState && - other.currentTime == currentTime && - other.duration == duration; - } - - @override - int get hashCode => - isCasting.hashCode ^ receiverName.hashCode ^ castState.hashCode ^ currentTime.hashCode ^ duration.hashCode; +@freezed +abstract class CastManagerState with _$CastManagerState { + const factory CastManagerState({ + required bool isCasting, + required String receiverName, + required CastState castState, + required Duration currentTime, + required Duration duration, + }) = _CastManagerState; } diff --git a/mobile/lib/models/download/download_state.model.dart b/mobile/lib/models/download/download_state.model.dart index bed92d98b6..7e47d60969 100644 --- a/mobile/lib/models/download/download_state.model.dart +++ b/mobile/lib/models/download/download_state.model.dart @@ -1,88 +1,20 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -import 'package:background_downloader/background_downloader.dart'; -import 'package:collection/collection.dart'; - -class DownloadInfo { - final String fileName; - final double progress; - // enum - final TaskStatus status; - - const DownloadInfo({required this.fileName, required this.progress, required this.status}); - - DownloadInfo copyWith({String? fileName, double? progress, TaskStatus? status}) { - return DownloadInfo( - fileName: fileName ?? this.fileName, - progress: progress ?? this.progress, - status: status ?? this.status, - ); - } - - Map toMap() { - return {'fileName': fileName, 'progress': progress, 'status': status.index}; - } - - factory DownloadInfo.fromMap(Map map) { - return DownloadInfo( - fileName: map['fileName'] as String, - progress: map['progress'] as double, - status: TaskStatus.values[map['status'] as int], - ); - } - - String toJson() => json.encode(toMap()); - - factory DownloadInfo.fromJson(String source) => DownloadInfo.fromMap(json.decode(source) as Map); - - @override - String toString() => 'DownloadInfo(fileName: $fileName, progress: $progress, status: $status)'; - - @override - bool operator ==(covariant DownloadInfo other) { - if (identical(this, other)) { - return true; - } - - return other.fileName == fileName && other.progress == progress && other.status == status; - } - - @override - int get hashCode => fileName.hashCode ^ progress.hashCode ^ status.hashCode; -} - -class DownloadState { - // enum - final TaskStatus downloadStatus; - final Map taskProgress; - final bool showProgress; - const DownloadState({required this.downloadStatus, required this.taskProgress, required this.showProgress}); - - DownloadState copyWith({TaskStatus? downloadStatus, Map? taskProgress, bool? showProgress}) { - return DownloadState( - downloadStatus: downloadStatus ?? this.downloadStatus, - taskProgress: taskProgress ?? this.taskProgress, - showProgress: showProgress ?? this.showProgress, - ); - } - - @override - String toString() => - 'DownloadState(downloadStatus: $downloadStatus, taskProgress: $taskProgress, showProgress: $showProgress)'; - - @override - bool operator ==(covariant DownloadState other) { - if (identical(this, other)) { - return true; - } - final mapEquals = const DeepCollectionEquality().equals; - - return other.downloadStatus == downloadStatus && - mapEquals(other.taskProgress, taskProgress) && - other.showProgress == showProgress; - } - - @override - int get hashCode => downloadStatus.hashCode ^ taskProgress.hashCode ^ showProgress.hashCode; -} +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:background_downloader/background_downloader.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'download_state.model.freezed.dart'; + +@freezed +abstract class DownloadInfo with _$DownloadInfo { + const factory DownloadInfo({required String fileName, required double progress, required TaskStatus status}) = + _DownloadInfo; +} + +@freezed +abstract class DownloadState with _$DownloadState { + const factory DownloadState({ + required TaskStatus downloadStatus, + required Map taskProgress, + required bool showProgress, + }) = _DownloadState; +} diff --git a/mobile/lib/models/search/search_curated_content.model.dart b/mobile/lib/models/search/search_curated_content.model.dart index 58c4c73264..8778c9b407 100644 --- a/mobile/lib/models/search/search_curated_content.model.dart +++ b/mobile/lib/models/search/search_curated_content.model.dart @@ -1,54 +1,20 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:convert'; - -/// A wrapper for [CuratedLocationsResponseDto] objects -/// and [CuratedObjectsResponseDto] to be displayed in -/// a view -class SearchCuratedContent { - /// The label to show associated with this curated object - final String label; - - /// The subtitle to show below the label - final String? subtitle; - - /// The id to lookup the asset from the server - final String id; - - const SearchCuratedContent({required this.label, required this.id, this.subtitle}); - - SearchCuratedContent copyWith({String? label, String? subtitle, String? id}) { - return SearchCuratedContent(label: label ?? this.label, subtitle: subtitle ?? this.subtitle, id: id ?? this.id); - } - - Map toMap() { - return {'label': label, 'subtitle': subtitle, 'id': id}; - } - - factory SearchCuratedContent.fromMap(Map map) { - return SearchCuratedContent( - label: map['label'] as String, - subtitle: map['subtitle'] as String?, - id: map['id'] as String, - ); - } - - String toJson() => json.encode(toMap()); - - factory SearchCuratedContent.fromJson(String source) => - SearchCuratedContent.fromMap(json.decode(source) as Map); - - @override - String toString() => 'CuratedContent(label: $label, subtitle: $subtitle, id: $id)'; - - @override - bool operator ==(covariant SearchCuratedContent other) { - if (identical(this, other)) { - return true; - } - - return other.label == label && other.subtitle == subtitle && other.id == id; - } - - @override - int get hashCode => label.hashCode ^ id.hashCode; -} +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'search_curated_content.model.freezed.dart'; + +/// A wrapper for [CuratedLocationsResponseDto] objects +/// and [CuratedObjectsResponseDto] to be displayed in +/// a view +@freezed +abstract class SearchCuratedContent with _$SearchCuratedContent { + const factory SearchCuratedContent({ + /// The label to show associated with this curated object + required String label, + + /// The id to lookup the asset from the server + required String id, + + /// The subtitle to show below the label + String? subtitle, + }) = _SearchCuratedContent; +} diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 825acb32c6..a9cc13ba6a 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -1,10 +1,13 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/utils/option.dart'; +part 'search_filter.model.freezed.dart'; + class SearchLocationFilter { String? country; String? state; @@ -177,52 +180,10 @@ class SearchRatingFilter { int get hashCode => rating.hashCode; } -class SearchDisplayFilters { - bool isNotInAlbum = false; - bool isArchive = false; - bool isFavorite = false; - SearchDisplayFilters({required this.isNotInAlbum, required this.isArchive, required this.isFavorite}); - - SearchDisplayFilters copyWith({bool? isNotInAlbum, bool? isArchive, bool? isFavorite}) { - return SearchDisplayFilters( - isNotInAlbum: isNotInAlbum ?? this.isNotInAlbum, - isArchive: isArchive ?? this.isArchive, - isFavorite: isFavorite ?? this.isFavorite, - ); - } - - Map toMap() { - return {'isNotInAlbum': isNotInAlbum, 'isArchive': isArchive, 'isFavorite': isFavorite}; - } - - factory SearchDisplayFilters.fromMap(Map map) { - return SearchDisplayFilters( - isNotInAlbum: map['isNotInAlbum'] as bool, - isArchive: map['isArchive'] as bool, - isFavorite: map['isFavorite'] as bool, - ); - } - - String toJson() => json.encode(toMap()); - - factory SearchDisplayFilters.fromJson(String source) => - SearchDisplayFilters.fromMap(json.decode(source) as Map); - - @override - String toString() => - 'SearchDisplayFilters(isNotInAlbum: $isNotInAlbum, isArchive: $isArchive, isFavorite: $isFavorite)'; - - @override - bool operator ==(covariant SearchDisplayFilters other) { - if (identical(this, other)) { - return true; - } - - return other.isNotInAlbum == isNotInAlbum && other.isArchive == isArchive && other.isFavorite == isFavorite; - } - - @override - int get hashCode => isNotInAlbum.hashCode ^ isArchive.hashCode ^ isFavorite.hashCode; +@freezed +abstract class SearchDisplayFilters with _$SearchDisplayFilters { + const factory SearchDisplayFilters({required bool isNotInAlbum, required bool isArchive, required bool isFavorite}) = + _SearchDisplayFilters; } class SearchFilter { @@ -233,7 +194,7 @@ class SearchFilter { String? language; String? assetId; List? tagIds; - Set people; + Set people; SearchLocationFilter location; SearchCameraFilter camera; SearchDateFilter date; @@ -289,7 +250,7 @@ class SearchFilter { String? language, String? ocr, String? assetId, - Set? people, + Set? people, List? tagIds, SearchLocationFilter? location, SearchCameraFilter? camera, diff --git a/mobile/lib/models/server_info/server_info.model.dart b/mobile/lib/models/server_info/server_info.model.dart index adc55d3f4e..1ed7dd6f8b 100644 --- a/mobile/lib/models/server_info/server_info.model.dart +++ b/mobile/lib/models/server_info/server_info.model.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/server_info/server_config.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; import 'package:immich_mobile/models/server_info/server_features.model.dart'; @@ -16,9 +16,9 @@ enum VersionStatus { String get message => switch (this) { VersionStatus.upToDate => "", - VersionStatus.clientOutOfDate => "app_update_available".tr(), - VersionStatus.serverOutOfDate => "server_update_available".tr(), - VersionStatus.error => "unable_to_check_version".tr(), + VersionStatus.clientOutOfDate => StaticTranslations.instance.app_update_available, + VersionStatus.serverOutOfDate => StaticTranslations.instance.server_update_available, + VersionStatus.error => StaticTranslations.instance.unable_to_check_version, }; } diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index e2472f1063..a0e55e58de 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; @@ -10,7 +9,6 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; @@ -108,7 +106,7 @@ class _DriftBackupPageState extends ConsumerState { return Scaffold( appBar: AppBar( elevation: 0, - title: Text("backup_controller_page_backup".t()), + title: Text(context.t.backup_controller_page_backup), leading: IconButton( onPressed: () { unawaited(context.maybePop(true)); @@ -122,7 +120,7 @@ class _DriftBackupPageState extends ConsumerState { unawaited(context.pushRoute(const DriftBackupOptionsRoute())); }, icon: const Icon(Icons.settings_outlined), - tooltip: "backup_options".t(context: context), + tooltip: context.t.backup_options, ), ], ), @@ -318,17 +316,16 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final backupAlbums = ref.watch(backupAlbumProvider); + Widget buildSelectedAlbumName() { - String text = "backup_controller_page_backup_selected".tr(); - final albums = ref - .read(backupAlbumProvider) - .where((album) => album.backupSelection == BackupSelection.selected) - .toList(); + String text = context.t.backup_controller_page_backup_selected; + final albums = backupAlbums.where((album) => album.backupSelection == BackupSelection.selected).toList(); if (albums.isNotEmpty) { for (var album in albums) { if (album.name == "Recent" || album.name == "Recents") { - text += "${album.name} (${'all'.tr()}), "; + text += "${album.name} (${context.t.all}), "; } else { text += "${album.name}, "; } @@ -345,7 +342,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( - "backup_controller_page_none_selected".tr(), + context.t.backup_controller_page_none_selected, style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), ), ); @@ -353,11 +350,8 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { } Widget buildExcludedAlbumName() { - String text = "backup_controller_page_excluded".tr(); - final albums = ref - .read(backupAlbumProvider) - .where((album) => album.backupSelection == BackupSelection.excluded) - .toList(); + String text = context.t.backup_controller_page_excluded; + final albums = backupAlbums.where((album) => album.backupSelection == BackupSelection.excluded).toList(); if (albums.isNotEmpty) { for (var album in albums) { @@ -385,16 +379,16 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { borderOnForeground: false, child: ListTile( minVerticalPadding: 18, - title: Text("backup_controller_page_albums", style: context.textTheme.titleMedium).tr(), + title: Text(context.t.backup_controller_page_albums, style: context.textTheme.titleMedium), subtitle: Padding( padding: const EdgeInsets.only(top: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "backup_controller_page_to_backup", + context.t.backup_controller_page_to_backup, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).tr(), + ), buildSelectedAlbumName(), buildExcludedAlbumName(), ], @@ -413,7 +407,7 @@ class _BackupAlbumSelectionCard extends ConsumerWidget { } unawaited(ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id)); }, - child: const Text("select", style: TextStyle(fontWeight: FontWeight.bold)).tr(), + child: Text(context.t.select, style: const TextStyle(fontWeight: FontWeight.bold)), ), ), ); @@ -428,8 +422,8 @@ class _TotalCard extends ConsumerWidget { final totalCount = ref.watch(driftBackupProvider.select((p) => p.totalCount)); return BackupInfoCard( - title: "total".tr(), - subtitle: "backup_controller_page_total_sub".tr(), + title: context.t.total, + subtitle: context.t.backup_controller_page_total_sub, info: totalCount.toString(), ); } @@ -444,8 +438,8 @@ class _BackupCard extends ConsumerWidget { final syncStatus = ref.watch(syncStatusProvider); return BackupInfoCard( - title: "backup_controller_page_backup".tr(), - subtitle: "backup_controller_page_backup_sub".tr(), + title: context.t.backup_controller_page_backup, + subtitle: context.t.backup_controller_page_backup_sub, info: backupCount.toString(), isLoading: syncStatus.isRemoteSyncing, ); @@ -472,11 +466,11 @@ class _RemainderCard extends ConsumerWidget { ListTile( minVerticalPadding: 18, isThreeLine: true, - title: Text("backup_controller_page_remainder".t(context: context), style: context.textTheme.titleMedium), + title: Text(context.t.backup_controller_page_remainder, style: context.textTheme.titleMedium), subtitle: Padding( padding: const EdgeInsets.only(top: 4.0, right: 18.0), child: Text( - "backup_controller_page_remainder_sub".t(context: context), + context.t.backup_controller_page_remainder_sub, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ), @@ -509,11 +503,11 @@ class _RemainderCard extends ConsumerWidget { ], ), Text( - "backup_info_card_assets", + context.t.backup_info_card_assets, style: context.textTheme.labelLarge?.copyWith( color: context.colorScheme.onSurface.withAlpha(syncStatus.isRemoteSyncing ? 50 : 255), ), - ).tr(), + ), ], ), ), @@ -530,7 +524,7 @@ class _RemainderCard extends ConsumerWidget { ), onTap: () => context.pushRoute(const DriftBackupAssetDetailRoute()), title: Text( - "view_details".t(context: context), + context.t.view_details, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), ), trailing: Icon(Icons.arrow_forward_ios, size: 16, color: context.colorScheme.onSurfaceVariant), @@ -622,7 +616,7 @@ class _PreparingStatusState extends ConsumerState { Row( children: [ Text( - "preparing".t(context: context), + context.t.preparing, style: context.textTheme.labelLarge?.copyWith( color: context.colorScheme.onSurface.withAlpha(200), ), @@ -652,7 +646,7 @@ class _PreparingStatusState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - "ready_for_upload".t(context: context), + context.t.ready_for_upload, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), ), const SizedBox(height: 2), diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart index de0ad52b5a..26719c2884 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -1,13 +1,12 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; @@ -154,13 +153,13 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState setState(() => _searchQuery = value.trim()), ) - : const Text("backup_album_selection_page_select_albums").t(context: context), + : Text(context.t.backup_album_selection_page_select_albums), actions: [ if (!_isSearchMode) IconButton( @@ -194,9 +193,9 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState> result = ref.watch(driftBackupCandidateProvider); return Scaffold( - appBar: AppBar(title: Text('backup_controller_page_remainder'.t(context: context))), + appBar: AppBar(title: Text(context.t.backup_controller_page_remainder)), body: result.when( data: (List candidates) { return ListView.separated( @@ -66,7 +65,7 @@ class DriftBackupAssetDetailPage extends ConsumerWidget { ); }, error: (error, stackTrace) => Text( - 'error_saving_image'.tr(args: [error.toString()]), + context.t.error_saving_image(error: error.toString()), style: TextStyle(color: context.colorScheme.error), ), loading: () => @@ -102,7 +101,7 @@ class DriftBackupAssetDetailPage extends ConsumerWidget { ); }, error: (Object error, StackTrace stackTrace) { - return Center(child: Text('error_saving_image'.tr(args: [error.toString()]))); + return Center(child: Text(context.t.error_saving_image(error: error.toString()))); }, loading: () { return const SizedBox(height: 48, width: 48, child: Center(child: CircularProgressIndicator.adaptive())); diff --git a/mobile/lib/pages/backup/drift_backup_options.page.dart b/mobile/lib/pages/backup/drift_backup_options.page.dart index ccd486e04d..994f09766c 100644 --- a/mobile/lib/pages/backup/drift_backup_options.page.dart +++ b/mobile/lib/pages/backup/drift_backup_options.page.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; @@ -51,10 +51,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { } ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("network_requirements_updated".t(context: context)), - duration: const Duration(seconds: 4), - ), + SnackBar(content: Text(context.t.network_requirements_updated), duration: const Duration(seconds: 4)), ); final backupNotifier = ref.read(driftBackupProvider.notifier); @@ -72,7 +69,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { } }, child: Scaffold( - appBar: AppBar(title: Text("backup_options".t(context: context))), + appBar: AppBar(title: Text(context.t.backup_options)), body: const DriftBackupSettings(), ), ); diff --git a/mobile/lib/pages/backup/drift_upload_detail.page.dart b/mobile/lib/pages/backup/drift_upload_detail.page.dart index 5ed43672d9..6993601c62 100644 --- a/mobile/lib/pages/backup/drift_upload_detail.page.dart +++ b/mobile/lib/pages/backup/drift_upload_detail.page.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; @@ -80,7 +80,7 @@ class _DriftUploadDetailPageState extends ConsumerState { return Scaffold( appBar: AppBar( - title: Text("upload_details".t(context: context)), + title: Text(context.t.upload_details), backgroundColor: context.colorScheme.surface, elevation: 0, scrolledUnderElevation: 1, @@ -125,7 +125,7 @@ class _DriftUploadDetailPageState extends ConsumerState { SliverToBoxAdapter( child: _buildSectionHeader( context, - title: "uploading".t(context: context), + title: context.t.uploading, count: uploadingItems.length, color: context.colorScheme.primary, ), @@ -151,7 +151,7 @@ class _DriftUploadDetailPageState extends ConsumerState { SliverToBoxAdapter( child: _buildSectionHeader( context, - title: "errors_text".t(context: context), + title: context.t.errors_text, count: failedItems.length, color: context.colorScheme.error, ), @@ -231,7 +231,7 @@ class _DriftUploadDetailPageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "downloading_from_icloud".t(context: context), + context.t.downloading_from_icloud, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), maxLines: 1, overflow: TextOverflow.ellipsis, @@ -318,7 +318,7 @@ class _DriftUploadDetailPageState extends ConsumerState { const SizedBox(height: 2), Text( isFailed - ? item.error ?? "unable_to_upload_file".t(context: context) + ? item.error ?? context.t.errors.unable_to_upload_file : "${formatHumanReadableBytes(item.fileSize, 1)} â€ĸ ${item.networkSpeedAsString}", style: context.textTheme.labelLarge?.copyWith( color: isFailed @@ -395,7 +395,7 @@ class _DriftUploadDetailPageState extends ConsumerState { ), const SizedBox(height: 4), Text( - item.error ?? "unable_to_upload_file".t(context: context), + item.error ?? context.t.errors.unable_to_upload_file, style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.error), maxLines: 4, overflow: TextOverflow.ellipsis, @@ -556,7 +556,7 @@ class FileDetailDialog extends ConsumerWidget { const SizedBox(width: 8), Expanded( child: Text( - "details".t(context: context), + context.t.details, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), ), ), @@ -595,20 +595,14 @@ class FileDetailDialog extends ConsumerWidget { const SizedBox(height: 24), if (asset != null) _buildInfoSection(context, [ - _buildInfoRow(context, "filename".t(context: context), path.basename(uploadStatus.filename)), - _buildInfoRow(context, "local_id".t(context: context), asset.id), - _buildInfoRow( - context, - "file_size".t(context: context), - formatHumanReadableBytes(uploadStatus.fileSize, 2), - ), - if (asset.width != null) _buildInfoRow(context, "width".t(context: context), "${asset.width}px"), - if (asset.height != null) - _buildInfoRow(context, "height".t(context: context), "${asset.height}px"), - _buildInfoRow(context, "created_at".t(context: context), asset.createdAt.toString()), - _buildInfoRow(context, "updated_at".t(context: context), asset.updatedAt.toString()), - if (asset.checksum != null) - _buildInfoRow(context, "checksum".t(context: context), asset.checksum!), + _buildInfoRow(context, context.t.filename, path.basename(uploadStatus.filename)), + _buildInfoRow(context, context.t.local_id, asset.id), + _buildInfoRow(context, context.t.file_size, formatHumanReadableBytes(uploadStatus.fileSize, 2)), + if (asset.width != null) _buildInfoRow(context, context.t.width, "${asset.width}px"), + if (asset.height != null) _buildInfoRow(context, context.t.height, "${asset.height}px"), + _buildInfoRow(context, context.t.created_at, asset.createdAt.toString()), + _buildInfoRow(context, context.t.updated_at, asset.updatedAt.toString()), + if (asset.checksum != null) _buildInfoRow(context, context.t.checksum, asset.checksum!), ]), ], ), @@ -620,7 +614,7 @@ class FileDetailDialog extends ConsumerWidget { TextButton( onPressed: () => Navigator.of(context).pop(), child: Text( - "close".t(), + context.t.close, style: TextStyle(fontWeight: FontWeight.w600, color: context.primaryColor), ), ), diff --git a/mobile/lib/pages/common/app_log.page.dart b/mobile/lib/pages/common/app_log.page.dart index 370d6a106d..d0b5096288 100644 --- a/mobile/lib/pages/common/app_log.page.dart +++ b/mobile/lib/pages/common/app_log.page.dart @@ -1,15 +1,16 @@ 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:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/immich_logger.service.dart'; +import 'package:intl/intl.dart'; @RoutePage() class AppLogPage extends HookWidget { @@ -50,7 +51,7 @@ class AppLogPage extends HookWidget { return Scaffold( appBar: AppBar( - title: Text('logs'.tr(), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)), + title: Text(context.t.logs, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)), scrolledUnderElevation: 1, elevation: 2, actions: [ diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index bc3d26a019..8e7eaf3ba8 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.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/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; @RoutePage() class AppLogDetailPage extends HookWidget { @@ -44,7 +44,7 @@ class AppLogDetailPage extends HookWidget { context.scaffoldMessenger.showSnackBar( SnackBar( content: Text( - "copied_to_clipboard".tr(), + context.t.copied_to_clipboard, style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), ), @@ -106,7 +106,7 @@ class AppLogDetailPage extends HookWidget { } return Scaffold( - appBar: AppBar(title: Text("log_detail_title".tr())), + appBar: AppBar(title: Text(context.t.log_detail_title)), body: SafeArea( child: ListView( children: [ diff --git a/mobile/lib/pages/common/download_panel.dart b/mobile/lib/pages/common/download_panel.dart index 2015948fad..9ee5833b83 100644 --- a/mobile/lib/pages/common/download_panel.dart +++ b/mobile/lib/pages/common/download_panel.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'package:background_downloader/background_downloader.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; class DownloadPanel extends ConsumerWidget { @@ -67,14 +67,14 @@ class DownloadTaskTile extends StatelessWidget { final progressPercent = (progress * 100).round(); String getStatusText() => switch (status) { - TaskStatus.running => 'downloading'.tr(), - TaskStatus.complete => 'download_complete'.tr(), - TaskStatus.failed => 'download_failed'.tr(), - TaskStatus.canceled => 'download_canceled'.tr(), - TaskStatus.paused => 'download_paused'.tr(), - TaskStatus.enqueued => 'download_enqueue'.tr(), - TaskStatus.notFound => 'download_notfound'.tr(), - TaskStatus.waitingToRetry => 'download_waiting_to_retry'.tr(), + TaskStatus.running => context.t.downloading, + TaskStatus.complete => context.t.download_complete, + TaskStatus.failed => context.t.download_failed, + TaskStatus.canceled => context.t.download_canceled, + TaskStatus.paused => context.t.download_paused, + TaskStatus.enqueued => context.t.download_enqueue, + TaskStatus.notFound => context.t.download_notfound, + TaskStatus.waitingToRetry => context.t.download_waiting_to_retry, }; return SizedBox( diff --git a/mobile/lib/pages/common/headers_settings.page.dart b/mobile/lib/pages/common/headers_settings.page.dart index e0228eecac..24c5da1c10 100644 --- a/mobile/lib/pages/common/headers_settings.page.dart +++ b/mobile/lib/pages/common/headers_settings.page.dart @@ -1,5 +1,4 @@ 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'; @@ -64,7 +63,7 @@ class HeaderSettingsPage extends HookConsumerWidget { headers.value = headers.value.toList(); }, icon: const Icon(Icons.add_outlined), - tooltip: 'header_settings_add_header_tip'.tr(), + tooltip: context.t.header_settings_add_header_tip, ), ], ), @@ -111,7 +110,7 @@ class HeaderKeyValueSettings extends StatelessWidget { String? emptyFieldValidator(String? value) { if (value == null || value.isEmpty) { - return 'header_settings_field_validator_msg'.tr(); + return StaticTranslations.instance.header_settings_field_validator_msg; } return null; @@ -129,7 +128,7 @@ class HeaderKeyValueSettings extends StatelessWidget { child: TextFormField( controller: keyController, decoration: InputDecoration( - labelText: 'header_settings_header_name_input'.tr(), + labelText: context.t.header_settings_header_name_input, border: const OutlineInputBorder(), ), autocorrect: false, @@ -157,7 +156,7 @@ class HeaderKeyValueSettings extends StatelessWidget { child: TextFormField( controller: valueController, decoration: InputDecoration( - labelText: 'header_settings_header_value_input'.tr(), + labelText: context.t.header_settings_header_value_input, border: const OutlineInputBorder(), ), autocorrect: false, diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index d102fe0d3b..2e941a70a3 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -1,5 +1,4 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -18,21 +17,45 @@ import 'package:immich_mobile/widgets/settings/preference_settings/preference_se import 'package:immich_mobile/widgets/settings/settings_card.dart'; enum SettingSection { - advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"), - assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"), - backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"), - freeUpSpace('free_up_space', Icons.cleaning_services_outlined, "free_up_space_settings_subtitle"), - languages('language', Icons.language, "setting_languages_subtitle"), - networking('networking_settings', Icons.wifi, "networking_subtitle"), - notifications('notifications', Icons.notifications_none_rounded, "setting_notifications_subtitle"), - preferences('preferences_settings_title', Icons.interests_outlined, "preferences_settings_subtitle"), - timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle"), - beta('sync_status', Icons.sync_outlined, "sync_status_subtitle"); + advanced(Icons.build_outlined), + assetViewer(Icons.image_outlined), + backup(Icons.cloud_upload_outlined), + freeUpSpace(Icons.cleaning_services_outlined), + languages(Icons.language), + networking(Icons.wifi), + notifications(Icons.notifications_none_rounded), + preferences(Icons.interests_outlined), + timeline(Icons.auto_awesome_mosaic_outlined), + beta(Icons.sync_outlined); - final String title; - final String subtitle; final IconData icon; + String title(Translations t) => switch (this) { + SettingSection.advanced => t.advanced, + SettingSection.assetViewer => t.asset_viewer_settings_title, + SettingSection.backup => t.backup, + SettingSection.freeUpSpace => t.free_up_space, + SettingSection.languages => t.language, + SettingSection.networking => t.networking_settings, + SettingSection.notifications => t.notifications, + SettingSection.preferences => t.preferences_settings_title, + SettingSection.timeline => t.asset_list_settings_title, + SettingSection.beta => t.sync_status, + }; + + String subtitle(Translations t) => switch (this) { + SettingSection.advanced => t.advanced_settings_tile_subtitle, + SettingSection.assetViewer => t.asset_viewer_settings_subtitle, + SettingSection.backup => t.backup_settings_subtitle, + SettingSection.freeUpSpace => t.free_up_space_settings_subtitle, + SettingSection.languages => t.setting_languages_subtitle, + SettingSection.networking => t.networking_subtitle, + SettingSection.notifications => t.setting_notifications_subtitle, + SettingSection.preferences => t.preferences_settings_subtitle, + SettingSection.timeline => t.asset_list_settings_subtitle, + SettingSection.beta => t.sync_status_subtitle, + }; + Widget get widget => switch (this) { SettingSection.advanced => const AdvancedSettings(), SettingSection.assetViewer => const AssetViewerSettings(), @@ -46,7 +69,7 @@ enum SettingSection { SettingSection.beta => const SyncStatusAndActions(), }; - const SettingSection(this.title, this.icon, this.subtitle); + const SettingSection(this.icon); } @RoutePage() @@ -55,9 +78,8 @@ class SettingsPage extends StatelessWidget { @override Widget build(BuildContext context) { - context.locale; return Scaffold( - appBar: AppBar(centerTitle: false, title: const Text('settings').tr()), + appBar: AppBar(centerTitle: false, title: Text(context.t.settings)), body: context.isMobile ? const _MobileLayout() : const _TabletLayout(), ); } @@ -73,15 +95,15 @@ class _MobileLayout extends StatelessWidget { ? [ SettingsCard( icon: Icons.sync_outlined, - title: 'sync_status'.tr(), - subtitle: 'sync_status_subtitle'.tr(), + title: context.t.sync_status, + subtitle: context.t.sync_status_subtitle, settingRoute: const SyncStatusRoute(), ), ] : [ SettingsCard( - title: setting.title.tr(), - subtitle: setting.subtitle.tr(), + title: setting.title(context.t), + subtitle: setting.subtitle(context.t), icon: setting.icon, settingRoute: SettingsSubRoute(section: setting), ), @@ -116,7 +138,7 @@ class _TabletLayout extends HookWidget { ...SettingSection.values.map( (s) => SliverToBoxAdapter( child: ListTile( - title: Text(s.title).tr(), + title: Text(s.title(context.t)), leading: Icon(s.icon), selected: s.index == selectedSection.value.index, selectedColor: context.primaryColor, @@ -127,7 +149,7 @@ class _TabletLayout extends HookWidget { ), SliverToBoxAdapter( child: ListTile( - title: Text('whats_new'.tr()), + title: Text(context.t.whats_new), leading: const Icon(Icons.auto_awesome_outlined), onTap: () => context.pushRoute(const WhatsNewRoute()), ), @@ -150,9 +172,8 @@ class SettingsSubPage extends StatelessWidget { @override Widget build(BuildContext context) { - context.locale; return Scaffold( - appBar: AppBar(centerTitle: false, title: Text(section.title).tr()), + appBar: AppBar(centerTitle: false, title: Text(section.title(context.t))), body: section.widget, ); } diff --git a/mobile/lib/pages/common/tab_shell.page.dart b/mobile/lib/pages/common/tab_shell.page.dart index f834e4ac51..240c4abe7a 100644 --- a/mobile/lib/pages/common/tab_shell.page.dart +++ b/mobile/lib/pages/common/tab_shell.page.dart @@ -1,18 +1,17 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/search/search_input_focus.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart'; @@ -34,24 +33,24 @@ class _TabShellPageState extends ConsumerState { final navigationDestinations = [ NavigationDestination( - label: 'photos'.tr(), + label: context.t.photos, icon: const Icon(Icons.photo_library_outlined), selectedIcon: Icon(Icons.photo_library, color: context.primaryColor), ), NavigationDestination( - label: 'search'.tr(), + label: context.t.search, icon: const Icon(Icons.search_rounded), selectedIcon: Icon(Icons.search, color: context.primaryColor), enabled: !isReadonlyModeEnabled, ), NavigationDestination( - label: 'albums'.tr(), + label: context.t.albums, icon: const Icon(Icons.photo_album_outlined), selectedIcon: Icon(Icons.photo_album_rounded, color: context.primaryColor), enabled: !isReadonlyModeEnabled, ), NavigationDestination( - label: 'library'.tr(), + label: context.t.library$, icon: const Icon(Icons.space_dashboard_outlined), selectedIcon: Icon(Icons.space_dashboard_rounded, color: context.primaryColor), enabled: !isReadonlyModeEnabled, @@ -132,7 +131,6 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) { // Library page if (index == kLibraryTabIndex) { ref.invalidate(localAlbumProvider); - ref.invalidate(driftGetAllPeopleProvider); } ref.read(hapticFeedbackProvider.notifier).selectionClick(); diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart index 69631efe4e..0264ceee16 100644 --- a/mobile/lib/pages/library/folder/folder.page.dart +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -10,6 +9,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/folder/recursive_folder.model.dart'; import 'package:immich_mobile/models/folder/root_folder.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; @@ -20,6 +20,7 @@ import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:intl/intl.dart'; RecursiveFolder? _findFolderInStructure(RootFolder rootFolder, RecursiveFolder targetFolder) { for (final folder in rootFolder.subfolders) { @@ -81,7 +82,7 @@ class FolderPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: Text(currentFolder.value?.name ?? tr("folders")), + title: Text(currentFolder.value?.name ?? context.t.folders), elevation: 0, centerTitle: false, actions: [IconButton(icon: const Icon(Icons.swap_vert), onPressed: onToggleSortOrder)], @@ -96,8 +97,8 @@ class FolderPage extends HookConsumerWidget { }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) { - ImmichToast.show(context: context, msg: "failed_to_load_folder".tr(), toastType: ToastType.error); - return Center(child: const Text("failed_to_load_folder").tr()); + ImmichToast.show(context: context, msg: context.t.failed_to_load_folder, toastType: ToastType.error); + return Center(child: Text(context.t.failed_to_load_folder)); }, ), ); @@ -125,16 +126,16 @@ class FolderContent extends HookConsumerWidget { }, [folder]); if (folder == null) { - return Center(child: const Text("folder_not_found").tr()); + return Center(child: Text(context.t.folder_not_found)); } String getSubtitle(int subFolderCount) { if (subFolderCount > 0) { - return "$subFolderCount ${tr("folders")}".toLowerCase(); + return "$subFolderCount ${context.t.folders}".toLowerCase(); } if (subFolderCount == 1) { - return "1 ${tr("folder")}".toLowerCase(); + return "1 ${context.t.folder}".toLowerCase(); } return ""; @@ -147,7 +148,7 @@ class FolderContent extends HookConsumerWidget { child: folderRenderlist.when( data: (folderAssets) { if (folder!.subfolders.isEmpty && folderAssets.isEmpty) { - return Center(child: const Text("empty_folder").tr()); + return Center(child: Text(context.t.empty_folder)); } return ListView( @@ -211,8 +212,8 @@ class FolderContent extends HookConsumerWidget { }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) { - ImmichToast.show(context: context, msg: "failed_to_load_assets".tr(), toastType: ToastType.error); - return Center(child: const Text("failed_to_load_assets").tr()); + ImmichToast.show(context: context, msg: context.t.failed_to_load_assets, toastType: ToastType.error); + return Center(child: Text(context.t.failed_to_load_assets)); }, ), ), diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index 81e08d853b..db0e009d8c 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -1,11 +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' show useState; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/local_auth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/forms/pin_registration_form.dart'; @@ -31,7 +31,7 @@ class PinAuthPage extends HookConsumerWidget { context.showSnackBar( SnackBar( - content: Text('biometric_auth_enabled'.tr(), style: context.textTheme.labelLarge), + content: Text(context.t.biometric_auth_enabled, style: context.textTheme.labelLarge), duration: const Duration(seconds: 3), backgroundColor: context.colorScheme.primaryContainer, ), @@ -52,7 +52,7 @@ class PinAuthPage extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ PinVerificationForm( - description: 'enable_biometric_auth_description'.tr(), + description: context.t.enable_biometric_auth_description, onSuccess: (pinCode) { Navigator.pop(buildContext); unawaited(registerBiometric(pinCode)); @@ -71,7 +71,7 @@ class PinAuthPage extends HookConsumerWidget { } return Scaffold( - appBar: AppBar(title: Text('locked_folder'.tr())), + appBar: AppBar(title: Text(context.t.locked_folder)), body: ListView( shrinkWrap: true, children: [ @@ -97,7 +97,7 @@ class PinAuthPage extends HookConsumerWidget { icon: const Icon(Icons.fingerprint, size: 28), onPressed: () => unawaited(enableBiometricAuth()), label: Text( - 'use_biometric'.tr(), + context.t.use_biometric, style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor, fontSize: 18), ), ), diff --git a/mobile/lib/pages/library/shared_link/shared_link.page.dart b/mobile/lib/pages/library/shared_link/shared_link.page.dart index 18fc235dd8..70cd4e2129 100644 --- a/mobile/lib/pages/library/shared_link/shared_link.page.dart +++ b/mobile/lib/pages/library/shared_link/shared_link.page.dart @@ -1,11 +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/generated/translations.g.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; import 'package:immich_mobile/providers/shared_link.provider.dart'; import 'package:immich_mobile/widgets/shared_link/shared_link_item.dart'; @@ -36,7 +36,7 @@ class SharedLinkPage extends HookConsumerWidget { children: [ Icon(Icons.link_off, size: 100, color: Theme.of(context).colorScheme.onSurface.withAlpha(128)), const SizedBox(height: 20), - const Text("you_dont_have_any_shared_links", style: TextStyle(fontSize: 14)).tr(), + Text(context.t.you_dont_have_any_shared_links, style: const TextStyle(fontSize: 14)), ], ), ); @@ -68,7 +68,7 @@ class SharedLinkPage extends HookConsumerWidget { } return Scaffold( - appBar: AppBar(title: const Text("shared_link_app_bar_title").tr(), elevation: 0, centerTitle: false), + appBar: AppBar(title: Text(context.t.shared_link_app_bar_title), elevation: 0, centerTitle: false), body: SafeArea( child: sharedLinks.widgetWhen( onError: (error, stackTrace) => buildNoShares(), diff --git a/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart index 23c24a64b0..6c90571428 100644 --- a/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart +++ b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart @@ -427,8 +427,8 @@ class SharedLinkEditPage extends HookConsumerWidget { return showDialog( context: context, builder: (BuildContext context) => ConfirmDialog( - title: "delete_shared_link_dialog_title", - content: "confirm_delete_shared_link", + title: context.t.delete_shared_link_dialog_title, + content: context.t.confirm_delete_shared_link, onOk: () async { await ref.read(sharedLinkServiceProvider).deleteSharedLink(existingLink!.id); ref.invalidate(sharedLinksStateProvider); diff --git a/mobile/lib/pages/search/map/map_location_picker.page.dart b/mobile/lib/pages/search/map/map_location_picker.page.dart index e8a507bfdc..b1d79b5150 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -2,12 +2,12 @@ import 'dart:async'; import 'dart:math'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/utils/map_utils.dart'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -148,10 +148,7 @@ class _BottomBar extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - ElevatedButton( - onPressed: onUseLocation, - child: const Text("map_location_picker_page_use_location").tr(), - ), + ElevatedButton(onPressed: onUseLocation, child: Text(context.t.map_location_picker_page_use_location)), ElevatedButton(onPressed: onGetCurrentLocation, child: const Icon(Icons.my_location)), ], ), diff --git a/mobile/lib/pages/settings/sync_status.page.dart b/mobile/lib/pages/settings/sync_status.page.dart index 58750e9e30..6a0af1230d 100644 --- a/mobile/lib/pages/settings/sync_status.page.dart +++ b/mobile/lib/pages/settings/sync_status.page.dart @@ -1,6 +1,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; @RoutePage() @@ -12,7 +12,7 @@ class SyncStatusPage extends StatelessWidget { return Scaffold( appBar: AppBar( elevation: 0, - title: const Text("sync_status").t(context: context), + title: Text(context.t.sync_status), leading: IconButton( onPressed: () => context.maybePop(true), splashRadius: 24, diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index db5e583d7a..5b651fbc8f 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; @@ -58,7 +58,7 @@ class ShareIntentPage extends ConsumerWidget { appBar: AppBar( title: Column( children: [ - const Text('upload_to_immich').tr(namedArgs: {'count': candidates.length.toString()}), + Text(context.t.upload_to_immich(count: candidates.length)), Text( currentEndpoint, style: context.textTheme.labelMedium?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), @@ -130,7 +130,7 @@ class ShareIntentPage extends ConsumerWidget { height: 48, child: ElevatedButton( onPressed: (isUploading || isUploaded) ? null : upload, - child: (isUploading || isUploaded) ? UploadingText(candidates: candidates) : const Text('upload').tr(), + child: (isUploading || isUploaded) ? UploadingText(candidates: candidates) : Text(context.t.upload), ), ), ), @@ -149,9 +149,7 @@ class UploadingText extends StatelessWidget { return element.status == UploadStatus.complete; }).length; - return const Text( - "shared_intent_upload_button_progress_text", - ).tr(namedArgs: {'current': uploadedCount.toString(), 'total': candidates.length.toString()}); + return Text(context.t.shared_intent_upload_button_progress_text(current: uploadedCount, total: candidates.length)); } } @@ -168,7 +166,7 @@ class UploadStatusIcon extends StatelessWidget { return Icon( Icons.check_circle_outline_rounded, color: context.colorScheme.onSurface.withAlpha(100), - semanticLabel: 'not_selected'.tr(), + semanticLabel: context.t.not_selected, ); } @@ -176,7 +174,7 @@ class UploadStatusIcon extends StatelessWidget { UploadStatus.enqueued => Icon( Icons.check_circle_rounded, color: context.primaryColor, - semanticLabel: 'enqueued'.tr(), + semanticLabel: context.t.enqueued, ), UploadStatus.running => Stack( alignment: AlignmentDirectional.center, @@ -191,7 +189,7 @@ class UploadStatusIcon extends StatelessWidget { backgroundColor: context.colorScheme.surfaceContainerLow, strokeWidth: 3, value: value, - semanticsLabel: 'uploading'.tr(), + semanticsLabel: context.t.uploading, ), ), ), @@ -201,8 +199,12 @@ class UploadStatusIcon extends StatelessWidget { ), ], ), - UploadStatus.complete => Icon(Icons.check_circle_rounded, color: Colors.green, semanticLabel: 'completed'.tr()), - UploadStatus.failed => Icon(Icons.error_rounded, color: Colors.red, semanticLabel: 'failed'.tr()), + UploadStatus.complete => Icon( + Icons.check_circle_rounded, + color: Colors.green, + semanticLabel: context.t.completed, + ), + UploadStatus.failed => Icon(Icons.error_rounded, color: Colors.red, semanticLabel: context.t.failed), }; return statusIcon; diff --git a/mobile/lib/presentation/actions/delete.action.dart b/mobile/lib/presentation/actions/delete.action.dart index 31d01b32ef..23a2ef2a2f 100644 --- a/mobile/lib/presentation/actions/delete.action.dart +++ b/mobile/lib/presentation/actions/delete.action.dart @@ -37,7 +37,8 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, final trashEnabled = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); // Assets already in the trash or in the locked folder are deleted outright, irrespective of the server setting. - final trash = trashEnabled && !ownedRemote.every((asset) => asset.isTrashed || asset.isLocked); + final trash = + ownedRemote.isEmpty || (trashEnabled && !ownedRemote.every((asset) => asset.isTrashed || asset.isLocked)); return (localIds: localIds, remoteIds: ownedRemote.map((asset) => asset.id).toList(growable: false), trash: trash); }, dependencies: [assetsActionProvider]); @@ -127,8 +128,11 @@ class DeleteAction extends AssetActionBuilder { final assetService = ref.read(assetServiceProvider); final confirmed = await showDialog( context: context, - builder: (_) => - const ConfirmDialog(title: 'delete_dialog_title', content: 'delete_dialog_alert', ok: 'delete_permanently'), + builder: (_) => ConfirmDialog( + title: context.t.delete_dialog_title, + content: context.t.delete_dialog_alert, + ok: context.t.delete_permanently, + ), ); if (confirmed != true || !context.mounted) { return null; diff --git a/mobile/lib/presentation/actions/similar_photos.action.dart b/mobile/lib/presentation/actions/similar_photos.action.dart index 3c0b7b129b..d8f63f245d 100644 --- a/mobile/lib/presentation/actions/similar_photos.action.dart +++ b/mobile/lib/presentation/actions/similar_photos.action.dart @@ -31,7 +31,7 @@ class SimilarPhotosAction extends ActionBuilder { location: .new(), camera: .new(), date: .new(), - display: .new(isNotInAlbum: false, isArchive: false, isFavorite: false), + display: const .new(isNotInAlbum: false, isArchive: false, isFavorite: false), rating: .new(), mediaType: .other, ), diff --git a/mobile/lib/presentation/pages/cleanup_preview.page.dart b/mobile/lib/presentation/pages/cleanup_preview.page.dart index 556ed6412f..45f8c80c91 100644 --- a/mobile/lib/presentation/pages/cleanup_preview.page.dart +++ b/mobile/lib/presentation/pages/cleanup_preview.page.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; @@ -19,7 +19,7 @@ class CleanupPreviewPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('cleanup_preview_title'.t(context: context, args: {'count': assets.length.toString()})), + title: Text(context.t.cleanup_preview_title(count: assets.length)), centerTitle: true, elevation: 0, scrolledUnderElevation: 0, diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart index 4c18a09200..b5a95156d8 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.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:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; @@ -56,7 +56,7 @@ class LocalMediaSummaryPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('local_media_summary'.tr())), + appBar: AppBar(title: Text(context.t.local_media_summary)), body: Consumer( builder: (ctx, ref, __) { final db = ref.watch(driftProvider); @@ -79,7 +79,7 @@ class LocalMediaSummaryPage extends StatelessWidget { const Divider(), Padding( padding: const EdgeInsets.only(left: 15), - child: Text("album_summary".tr(), style: ctx.textTheme.titleMedium), + child: Text(ctx.t.album_summary, style: ctx.textTheme.titleMedium), ), ], ), @@ -136,7 +136,7 @@ class RemoteMediaSummaryPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('remote_media_summary'.tr())), + appBar: AppBar(title: Text(context.t.remote_media_summary)), body: Consumer( builder: (ctx, ref, __) { final db = ref.watch(driftProvider); @@ -159,7 +159,7 @@ class RemoteMediaSummaryPage extends StatelessWidget { const Divider(), Padding( padding: const EdgeInsets.only(left: 15), - child: Text("album_summary".tr(), style: ctx.textTheme.titleMedium), + child: Text(ctx.t.album_summary, style: ctx.textTheme.titleMedium), ), ], ), diff --git a/mobile/lib/presentation/pages/download_info.page.dart b/mobile/lib/presentation/pages/download_info.page.dart index 8073d3098c..09a65b4586 100644 --- a/mobile/lib/presentation/pages/download_info.page.dart +++ b/mobile/lib/presentation/pages/download_info.page.dart @@ -4,7 +4,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/pages/common/download_panel.dart'; import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; @@ -21,10 +21,7 @@ class DownloadInfoPage extends ConsumerWidget { } return Scaffold( - appBar: AppBar( - title: Text("download".t(context: context)), - actions: const [], - ), + appBar: AppBar(title: Text(context.t.download), actions: const []), body: ListView.builder( physics: const ClampingScrollPhysics(), shrinkWrap: true, @@ -49,7 +46,7 @@ class DownloadInfoPage extends ConsumerWidget { }, style: OutlinedButton.styleFrom(side: BorderSide(color: context.colorScheme.primary)), child: Text( - 'clear_all'.t(context: context), + context.t.clear_all, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary), ), ), diff --git a/mobile/lib/presentation/pages/drift_album_options.page.dart b/mobile/lib/presentation/pages/drift_album_options.page.dart index 7b4c48a6b0..078b9cc325 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/drift_album_options.page.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; @@ -32,12 +32,14 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { final userId = ref.watch(authProvider).userId; final activityEnabled = useState(album.isActivityEnabled); final isOwner = album.ownerId == userId; + final owner = isOwner ? ref.watch(currentUserProvider) : null; + final allUsers = isOwner ? null : ref.watch(driftUsersProvider); void showErrorMessage() { ContextHelper(context).pop(); ImmichToast.show( context: context, - msg: "shared_album_section_people_action_error".t(context: context), + msg: context.t.shared_album_section_people_action_error, toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); @@ -87,7 +89,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { ImmichToast.show( context: context, - msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}), + msg: context.t.users_added_to_album_count(count: newUsers.length), toastType: ToastType.success, ); } catch (e) { @@ -106,7 +108,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { actions = [ ListTile( leading: const Icon(Icons.exit_to_app_rounded), - title: const Text("leave_album").t(context: context), + title: Text(context.t.leave_album), onTap: leaveAlbum, ), ]; @@ -116,7 +118,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { actions = [ ListTile( leading: const Icon(Icons.person_remove_rounded), - title: const Text("remove_user").t(context: context), + title: Text(context.t.remove_user), onTap: () => removeUserFromAlbum(user), ), ]; @@ -141,16 +143,18 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { Widget buildOwnerInfo() { if (isOwner) { - final owner = ref.read(currentUserProvider); return ListTile( leading: owner != null ? UserCircleAvatar(user: owner) : const SizedBox(), title: Text(album.ownerName, style: const TextStyle(fontWeight: FontWeight.w500)), subtitle: Text(owner?.email ?? "", style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context), + trailing: Text(context.t.owner, style: context.textTheme.labelLarge), ); } else { - final usersProvider = ref.read(driftUsersProvider); - return usersProvider.maybeWhen( + if (allUsers == null) { + return const SizedBox(); + } + + return allUsers.maybeWhen( data: (users) { final user = users.firstWhereOrNull((u) => u.id == album.ownerId); @@ -162,7 +166,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { leading: UserCircleAvatar(user: user), title: Text(user.name, style: const TextStyle(fontWeight: FontWeight.w500)), subtitle: Text(user.email, style: TextStyle(color: context.colorScheme.onSurfaceSecondary)), - trailing: Text("owner", style: context.textTheme.labelLarge).t(context: context), + trailing: Text(context.t.owner, style: context.textTheme.labelLarge), ); }, orElse: () => const SizedBox(), @@ -207,7 +211,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { onPressed: () => context.maybePop(null), ), centerTitle: true, - title: Text("options".t(context: context)), + title: Text(context.t.options), ), body: ListView( children: [ @@ -222,19 +226,19 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { activeThumbColor: activityEnabled.value ? context.primaryColor : context.themeData.disabledColor, dense: true, title: Text( - "comments_and_likes", + context.t.comments_and_likes, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500), - ).t(context: context), + ), subtitle: Text( - "let_others_respond", + context.t.let_others_respond, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), - ).t(context: context), + ), ), - buildSectionTitle("shared_album_section_people_title".t(context: context)), + buildSectionTitle(context.t.shared_album_section_people_title), if (isOwner) ...[ ListTile( leading: const Icon(Icons.person_add_rounded), - title: Text("invite_people".t(context: context)), + title: Text(context.t.invite_people), onTap: () async => addUsers(), ), const Divider(indent: 16), diff --git a/mobile/lib/presentation/pages/drift_archive.page.dart b/mobile/lib/presentation/pages/drift_archive.page.dart index ee8417bcad..75c05eb070 100644 --- a/mobile/lib/presentation/pages/drift_archive.page.dart +++ b/mobile/lib/presentation/pages/drift_archive.page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; @@ -28,10 +28,7 @@ class DriftArchivePage extends StatelessWidget { }), ], child: Timeline( - appBar: MesmerizingSliverAppBar( - title: 'archive'.t(context: context), - icon: Icons.archive_outlined, - ), + appBar: MesmerizingSliverAppBar(title: context.t.archive, icon: Icons.archive_outlined), bottomSheet: const ArchiveBottomSheet(), ), ); diff --git a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart index 7e98d25cf0..81d765239b 100644 --- a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart +++ b/mobile/lib/presentation/pages/drift_asset_troubleshoot.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/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; @RoutePage() @@ -18,7 +18,7 @@ class AssetTroubleshootPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('asset_troubleshoot'.tr())), + appBar: AppBar(title: Text(context.t.asset_troubleshoot)), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(16.0), @@ -42,7 +42,7 @@ class _AssetDetailsView extends StatelessWidget { _AssetPropertiesSection(asset: asset), const SizedBox(height: 16), Text( - 'matching_assets'.tr(), + context.t.matching_assets, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), if (asset.checksum != null) ...[ @@ -52,12 +52,12 @@ class _AssetDetailsView extends StatelessWidget { ] else ...[ _PropertySectionCard( title: 'Local Assets', - properties: [_PropertyItem(label: 'Status', value: 'no_checksum_local'.tr())], + properties: [_PropertyItem(label: 'Status', value: context.t.no_checksum_local)], ), const SizedBox(height: 16), _PropertySectionCard( title: 'Remote Assets', - properties: [_PropertyItem(label: 'Status', value: 'no_checksum_remote'.tr())], + properties: [_PropertyItem(label: 'Status', value: context.t.no_checksum_remote)], ), ], ], @@ -243,7 +243,7 @@ class _LocalAssetsSection extends ConsumerWidget { if (localAssets.isEmpty) { return _PropertySectionCard( title: 'Local Assets', - properties: [_PropertyItem(label: 'Status', value: 'no_local_assets_found'.tr())], + properties: [_PropertyItem(label: 'Status', value: context.t.no_local_assets_found)], ); } @@ -302,7 +302,7 @@ class _RemoteAssetSection extends ConsumerWidget { if (remoteAsset == null) { return _PropertySectionCard( title: 'Remote Assets', - properties: [_PropertyItem(label: 'Status', value: 'no_remote_assets_found'.tr())], + properties: [_PropertyItem(label: 'Status', value: context.t.no_remote_assets_found)], ); } @@ -356,7 +356,7 @@ class _PropertyItem extends StatelessWidget { ), Expanded( child: Text( - value ?? 'not_available'.tr(), + value ?? context.t.not_available, style: TextStyle(color: Theme.of(context).colorScheme.secondary), ), ), diff --git a/mobile/lib/presentation/pages/drift_create_album.page.dart b/mobile/lib/presentation/pages/drift_create_album.page.dart index 93534d62a5..f536b5b60f 100644 --- a/mobile/lib/presentation/pages/drift_create_album.page.dart +++ b/mobile/lib/presentation/pages/drift_create_album.page.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -52,9 +52,7 @@ class _DriftCreateAlbumPageState extends ConsumerState { bool get _canCreateAlbum => albumTitleController.text.trim().isNotEmpty && !isCreatingAlbum; String _getEffectiveTitle() { - return albumTitleController.text.isNotEmpty - ? albumTitleController.text - : 'create_album_page_untitled'.t(context: context); + return albumTitleController.text.isNotEmpty ? albumTitleController.text : context.t.create_album_page_untitled; } Widget _buildSliverAppBar() { @@ -93,7 +91,7 @@ class _DriftCreateAlbumPageState extends ConsumerState { Widget _buildEmptyState() { return Padding( padding: const EdgeInsets.only(top: 0, left: 18), - child: Text('create_shared_album_page_share_add_assets', style: context.textTheme.labelLarge).t(), + child: Text(context.t.create_shared_album_page_share_add_assets, style: context.textTheme.labelLarge), ); } @@ -112,9 +110,9 @@ class _DriftCreateAlbumPageState extends ConsumerState { label: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( - 'create_shared_album_page_share_select_photos', + context.t.create_shared_album_page_share_select_photos, style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), - ).t(), + ), ), ), ); @@ -148,7 +146,7 @@ class _DriftCreateAlbumPageState extends ConsumerState { }); if (albumTitleController.text.isEmpty) { - final untitledText = 'create_album_page_untitled'.t(); + final untitledText = context.t.create_album_page_untitled; albumTitleController.text = untitledText; } } @@ -196,7 +194,7 @@ class _DriftCreateAlbumPageState extends ConsumerState { return; } - ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.t()); + ImmichToast.show(context: context, toastType: ToastType.error, msg: context.t.errors.failed_to_create_album); } finally { if (mounted) { setState(() => isCreatingAlbum = false); @@ -241,7 +239,7 @@ class _DriftCreateAlbumPageState extends ConsumerState { AlbumActionFilledButton( iconData: Icons.add_photo_alternate_outlined, onPressed: onSelectPhotos, - labelText: "add_photos".t(), + labelText: context.t.add_photos, ), ], ), @@ -257,12 +255,12 @@ class _DriftCreateAlbumPageState extends ConsumerState { centerTitle: false, backgroundColor: context.scaffoldBackgroundColor, leading: IconButton(onPressed: () => context.maybePop(), icon: const Icon(Icons.close_rounded)), - title: const Text('create_album').t(), + title: Text(context.t.create_album), actions: [ TextButton( onPressed: _canCreateAlbum ? createAlbum : null, child: Text( - 'create'.t(), + context.t.create, style: TextStyle( fontWeight: FontWeight.bold, color: _canCreateAlbum ? context.primaryColor : context.themeData.disabledColor, @@ -320,7 +318,7 @@ class _AlbumTitleTextFieldState extends State<_AlbumTitleTextField> { style: TextStyle(fontSize: 28.0, color: context.colorScheme.onSurface, fontWeight: FontWeight.bold), controller: widget.textController, onTap: () { - if (widget.textController.text == 'create_album_page_untitled'.t(context: context)) { + if (widget.textController.text == context.t.create_album_page_untitled) { widget.textController.clear(); } }, @@ -343,7 +341,7 @@ class _AlbumTitleTextFieldState extends State<_AlbumTitleTextField> { borderSide: BorderSide(color: context.primaryColor.withValues(alpha: 0.3)), borderRadius: const BorderRadius.all(Radius.circular(16.0)), ), - hintText: 'add_a_title'.t(), + hintText: context.t.add_a_title, hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( fontSize: 28.0, fontWeight: FontWeight.bold, @@ -430,7 +428,7 @@ class _AlbumViewerEditableDescriptionState extends State<_AlbumViewerEditableDes focusColor: Colors.grey[300], fillColor: context.scaffoldBackgroundColor, filled: widget.focusNode.hasFocus, - hintText: 'add_a_description'.t(), + hintText: context.t.add_a_description, ), ), ); diff --git a/mobile/lib/presentation/pages/drift_favorite.page.dart b/mobile/lib/presentation/pages/drift_favorite.page.dart index 2bc3f26363..dd4384906b 100644 --- a/mobile/lib/presentation/pages/drift_favorite.page.dart +++ b/mobile/lib/presentation/pages/drift_favorite.page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; @@ -28,10 +28,7 @@ class DriftFavoritePage extends StatelessWidget { }), ], child: Timeline( - appBar: MesmerizingSliverAppBar( - title: 'favorites'.t(context: context), - icon: Icons.favorite_outline, - ), + appBar: MesmerizingSliverAppBar(title: context.t.favorites, icon: Icons.favorite_outline), bottomSheet: const FavoriteBottomSheet(), ), ); diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart index 740de441e7..137f46659f 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.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/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/local_album_thumbnail.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/people/partner_user_avatar.widget.dart'; @@ -56,13 +55,13 @@ class _ActionButtonGrid extends ConsumerWidget { _ActionButton( icon: Icons.favorite_outline_rounded, onTap: () => context.pushRoute(const DriftFavoriteRoute()), - label: 'favorites'.t(context: context), + label: context.t.favorites, ), const SizedBox(width: 8), _ActionButton( icon: Icons.archive_outlined, onTap: () => context.pushRoute(const DriftArchiveRoute()), - label: 'archived'.t(context: context), + label: context.t.archived, ), ], ), @@ -72,14 +71,14 @@ class _ActionButtonGrid extends ConsumerWidget { _ActionButton( icon: Icons.link_outlined, onTap: () => context.pushRoute(const SharedLinkRoute()), - label: 'shared_links'.t(context: context), + label: context.t.shared_links, ), isTrashEnable ? const SizedBox(width: 8) : const SizedBox.shrink(), isTrashEnable ? _ActionButton( icon: Icons.delete_outline_rounded, onTap: () => context.pushRoute(const DriftTrashRoute()), - label: 'trash'.t(context: context), + label: context.t.trash, ) : const SizedBox.shrink(), ], @@ -146,7 +145,7 @@ class _PeopleCollectionCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final people = ref.watch(driftGetAllPeopleProvider); + final people = ref.watch(getAllPeopleProvider); return LayoutBuilder( builder: (context, constraints) { @@ -193,7 +192,7 @@ class _PeopleCollectionCard extends ConsumerWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'people'.t(context: context), + context.t.people, style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, @@ -245,7 +244,7 @@ class _PlacesCollectionCard extends StatelessWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'places'.t(), + context.t.places, style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, @@ -303,9 +302,7 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget { }).toList(); }, error: (error, _) { - return [ - Center(child: Text('error_saving_image'.tr(args: [error.toString()]))), - ]; + return [Center(child: Text(context.t.error_saving_image(error: error.toString())))]; }, loading: () { return [const Center(child: CircularProgressIndicator())]; @@ -317,7 +314,7 @@ class _LocalAlbumsCollectionCard extends ConsumerWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'on_this_device'.t(context: context), + context.t.on_this_device, style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, @@ -384,7 +381,7 @@ class _QuickAccessButtonList extends ConsumerWidget { ), leading: const Icon(Icons.folder_outlined, size: 26), title: Text( - 'folders'.t(context: context), + context.t.folders, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), ), onTap: () => context.pushRoute(FolderRoute()), @@ -392,7 +389,7 @@ class _QuickAccessButtonList extends ConsumerWidget { ListTile( leading: const Icon(Icons.lock_outline_rounded, size: 26), title: Text( - 'locked_folder'.t(context: context), + context.t.locked_folder, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), ), onTap: () => context.pushRoute(const DriftLockedFolderRoute()), @@ -400,7 +397,7 @@ class _QuickAccessButtonList extends ConsumerWidget { ListTile( leading: const Icon(Icons.group_outlined, size: 26), title: Text( - 'partners'.t(context: context), + context.t.partners, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500), ), onTap: () => context.pushRoute(const PartnerRoute()), @@ -438,10 +435,10 @@ class _PartnerList extends StatelessWidget { ), contentPadding: const EdgeInsets.only(left: 12.0, right: 18.0), leading: PartnerUserAvatar(userId: partner.id, name: partner.name), - title: const Text( - "partner_list_user_photos", - style: TextStyle(fontWeight: FontWeight.w500), - ).t(context: context, args: {'user': partner.name}), + title: Text( + context.t.partner_list_user_photos(user: partner.name), + style: const TextStyle(fontWeight: FontWeight.w500), + ), onTap: () => context.pushRoute(DriftPartnerDetailRoute(partner: partner)), ); }, diff --git a/mobile/lib/presentation/pages/drift_local_album.page.dart b/mobile/lib/presentation/pages/drift_local_album.page.dart index 6b133f7a6f..71707688da 100644 --- a/mobile/lib/presentation/pages/drift_local_album.page.dart +++ b/mobile/lib/presentation/pages/drift_local_album.page.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:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/presentation/widgets/images/local_album_thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; @@ -49,7 +48,7 @@ class _AlbumList extends ConsumerWidget { if (albums.isEmpty) { return SliverToBoxAdapter( child: Center( - child: Padding(padding: const EdgeInsets.all(20.0), child: Text('no_albums_yet'.tr())), + child: Padding(padding: const EdgeInsets.all(20.0), child: Text(context.t.no_albums_yet)), ), ); } @@ -67,7 +66,7 @@ class _AlbumList extends ConsumerWidget { leading: SizedBox(width: 80, height: 80, child: LocalAlbumThumbnail(albumId: album.id)), title: Text(album.name, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)), subtitle: Text( - 'items_count'.t(context: context, args: {'count': album.assetCount}), + context.t.items_count(count: album.assetCount), style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), onTap: () => context.pushRoute(LocalTimelineRoute(album: album)), diff --git a/mobile/lib/presentation/pages/drift_locked_folder.page.dart b/mobile/lib/presentation/pages/drift_locked_folder.page.dart index e8130f7059..6ebe7283d3 100644 --- a/mobile/lib/presentation/pages/drift_locked_folder.page.dart +++ b/mobile/lib/presentation/pages/drift_locked_folder.page.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; @@ -70,7 +70,7 @@ class _DriftLockedFolderPageState extends ConsumerState w : PopScope( onPopInvokedWithResult: (didPop, _) => didPop ? ref.read(authProvider.notifier).lockPinCode() : null, child: Timeline( - appBar: MesmerizingSliverAppBar(title: 'locked_folder'.t(context: context)), + appBar: MesmerizingSliverAppBar(title: context.t.locked_folder), bottomSheet: const LockedFolderBottomSheet(), ), ), diff --git a/mobile/lib/presentation/pages/drift_memory.page.dart b/mobile/lib/presentation/pages/drift_memory.page.dart index 31371fe581..3ad7d90657 100644 --- a/mobile/lib/presentation/pages/drift_memory.page.dart +++ b/mobile/lib/presentation/pages/drift_memory.page.dart @@ -8,7 +8,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/memory/memory_bottom_info.widget.dart'; import 'package:immich_mobile/presentation/widgets/memory/memory_card.widget.dart'; @@ -248,7 +248,7 @@ class DriftMemoryPage extends HookConsumerWidget { } final yearsAgo = DateTime.now().year - memories[mIndex].data.year; - final title = 'years_ago'.t(context: context, args: {'years': yearsAgo.toString()}); + final title = context.t.years_ago(years: yearsAgo); // Build horizontal page final assetController = memoryAssetPageControllers[mIndex]; return Column( diff --git a/mobile/lib/presentation/pages/drift_people_collection.page.dart b/mobile/lib/presentation/pages/drift_people_collection.page.dart index 416b7d587a..569cb99b01 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/drift_people_collection.page.dart @@ -1,11 +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:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -33,7 +33,7 @@ class _DriftPeopleCollectionPageState extends ConsumerState _formFocus.unfocus(), onChanged: (value) => setState(() => _search = value), filled: true, - hintText: 'filter_people'.tr(), + hintText: context.t.filter_people, autofocus: true, ) - : Text('people'.tr()), + : Text(context.t.people), actions: [ IconButton( icon: Icon(_search != null ? Icons.close : Icons.search), @@ -107,7 +107,7 @@ class _DriftPeopleCollectionPageState extends ConsumerState showNameEditModal(context, person), child: person.name.isEmpty ? Text( - 'add_a_name'.tr(), + context.t.add_a_name, style: context.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, color: context.colorScheme.primary, diff --git a/mobile/lib/presentation/pages/drift_person.page.dart b/mobile/lib/presentation/pages/drift_person.page.dart index 7389f506d5..cad3b7b6a6 100644 --- a/mobile/lib/presentation/pages/drift_person.page.dart +++ b/mobile/lib/presentation/pages/drift_person.page.dart @@ -14,7 +14,7 @@ import 'package:immich_mobile/widgets/common/person_sliver_app_bar.dart'; @RoutePage() class DriftPersonPage extends ConsumerStatefulWidget { - final DriftPerson person; + final Person person; const DriftPersonPage({super.key, required this.person}); @@ -23,7 +23,7 @@ class DriftPersonPage extends ConsumerStatefulWidget { } class _DriftPersonPageState extends ConsumerState { - late DriftPerson _person; + late Person _person; @override void initState() { diff --git a/mobile/lib/presentation/pages/drift_place.page.dart b/mobile/lib/presentation/pages/drift_place.page.dart index 10b9ca7ae4..68824e2752 100644 --- a/mobile/lib/presentation/pages/drift_place.page.dart +++ b/mobile/lib/presentation/pages/drift_place.page.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; @@ -62,10 +62,10 @@ class _PlaceSliverAppBar extends HookWidget { onTapOutside: (_) => searchFocusNode.unfocus(), onChanged: (value) => search.value = value, filled: true, - hintText: 'filter_places'.t(context: context), + hintText: context.t.filter_places, autofocus: true, ) - : Text('places'.t(context: context)), + : Text(context.t.places), actions: [ IconButton( icon: Icon(search.value != null ? Icons.close : Icons.search), diff --git a/mobile/lib/presentation/pages/drift_recently_added.page.dart b/mobile/lib/presentation/pages/drift_recently_added.page.dart index c33cd4370d..841fb9a671 100644 --- a/mobile/lib/presentation/pages/drift_recently_added.page.dart +++ b/mobile/lib/presentation/pages/drift_recently_added.page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -26,7 +26,7 @@ class DriftRecentlyAddedPage extends StatelessWidget { return timelineService; }), ], - child: Timeline(appBar: MesmerizingSliverAppBar(title: 'recently_added'.t())), + child: Timeline(appBar: MesmerizingSliverAppBar(title: context.t.recently_added)), ); } } diff --git a/mobile/lib/presentation/pages/drift_recently_taken.page.dart b/mobile/lib/presentation/pages/drift_recently_taken.page.dart index ceb121b124..22d89770fe 100644 --- a/mobile/lib/presentation/pages/drift_recently_taken.page.dart +++ b/mobile/lib/presentation/pages/drift_recently_taken.page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -26,7 +26,7 @@ class DriftRecentlyTakenPage extends StatelessWidget { return timelineService; }), ], - child: Timeline(appBar: MesmerizingSliverAppBar(title: 'recently_taken'.t())), + child: Timeline(appBar: MesmerizingSliverAppBar(title: context.t.recently_taken)), ); } } diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index f9e59da0a3..7669b6c765 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -7,7 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/album/pending_uploads_banner.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/remote_album/drift_album_option.widget.dart'; @@ -62,7 +62,7 @@ class _RemoteAlbumPageState extends ConsumerState { if (added > 0) { ImmichToast.show( context: context, - msg: "assets_added_to_album_count".t(context: context, args: {'count': added.toString()}), + msg: context.t.assets_added_to_album_count(count: added), toastType: ToastType.success, ); } @@ -88,7 +88,7 @@ class _RemoteAlbumPageState extends ConsumerState { ImmichToast.show( context: context, - msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}), + msg: context.t.users_added_to_album_count(count: newUsers.length), toastType: ToastType.success, ); } catch (e) { @@ -111,24 +111,21 @@ class _RemoteAlbumPageState extends ConsumerState { context: context, builder: (BuildContext context) { return AlertDialog( - title: Text('delete_album'.t(context: context)), + title: Text(context.t.delete_album), content: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('album_delete_confirmation'.t(context: context, args: {'album': _album.name})), + Text(context.t.album_delete_confirmation(album: _album.name)), const SizedBox(height: 8), - Text('album_delete_confirmation_description'.t(context: context)), + Text(context.t.album_delete_confirmation_description), ], ), actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text('cancel'.t(context: context)), - ), + TextButton(onPressed: () => Navigator.of(context).pop(false), child: Text(context.t.cancel)), TextButton( onPressed: () => Navigator.of(context).pop(true), style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error), - child: Text('delete_album'.t(context: context)), + child: Text(context.t.delete_album), ), ], ); @@ -146,11 +143,7 @@ class _RemoteAlbumPageState extends ConsumerState { return; } - ImmichToast.show( - context: context, - msg: 'album_deleted'.t(context: context), - toastType: ToastType.success, - ); + ImmichToast.show(context: context, msg: context.t.album_deleted, toastType: ToastType.success); unawaited(context.pushRoute(const DriftAlbumsRoute())); } catch (e) { @@ -160,7 +153,7 @@ class _RemoteAlbumPageState extends ConsumerState { ImmichToast.show( context: context, - msg: 'album_viewer_appbar_share_err_delete'.t(context: context), + msg: context.t.album_viewer_appbar_share_err_delete, toastType: ToastType.error, ); } @@ -288,11 +281,7 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { return; } - ImmichToast.show( - context: context, - msg: 'album_update_error'.t(context: context), - toastType: ToastType.error, - ); + ImmichToast.show(context: context, msg: context.t.errors.unable_to_update_album_info, toastType: ToastType.error); } } @@ -315,14 +304,14 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { children: [ Icon(Icons.edit_outlined, color: context.colorScheme.primary, size: 24), const SizedBox(width: 12), - Text('edit_album'.t(context: context), style: context.textTheme.titleMedium), + Text(context.t.edit_album, style: context.textTheme.titleMedium), ], ), const SizedBox(height: 24), // Album Name Text( - 'album_name'.t(context: context).toUpperCase(), + context.t.album_name.toUpperCase(), style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 4), @@ -337,7 +326,7 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { ), validator: (value) { if (value == null || value.trim().isEmpty) { - return 'album_name_required'.t(context: context); + return context.t.name_required; } return null; @@ -347,7 +336,7 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { // Description Text( - 'description'.t(context: context).toUpperCase(), + context.t.description.toUpperCase(), style: context.textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 4), @@ -367,15 +356,9 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - TextButton( - onPressed: () => Navigator.of(context).pop(null), - child: Text('cancel'.t(context: context)), - ), + TextButton(onPressed: () => Navigator.of(context).pop(null), child: Text(context.t.cancel)), const SizedBox(width: 12), - FilledButton( - onPressed: _handleSave, - child: Text('save'.t(context: context)), - ), + FilledButton(onPressed: _handleSave, child: Text(context.t.save)), ], ), ], diff --git a/mobile/lib/presentation/pages/drift_slideshow.page.dart b/mobile/lib/presentation/pages/drift_slideshow.page.dart index 48b29b3499..2662c60a30 100644 --- a/mobile/lib/presentation/pages/drift_slideshow.page.dart +++ b/mobile/lib/presentation/pages/drift_slideshow.page.dart @@ -7,11 +7,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/config/slideshow_config.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/scroll_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; @@ -295,14 +296,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si color: context.colorScheme.primary, ); } else { - return LinearProgressIndicator( - color: context.colorScheme.primary, - borderRadius: BorderRadius.zero, - minHeight: 5, - value: - ref.read(videoPlayerProvider(asset.id).select((s) => s.position)).inMilliseconds / - asset.duration.inMilliseconds, - ); + return _VideoProgressBar(asset: asset); } } @@ -374,25 +368,13 @@ class _DriftSlideshowPageState extends ConsumerState with Si builder: (context, value, _) => buildPhotoView(scale * (1.0 + value * _kenBurnsZoom)), ); } else { - final status = ref.read(videoPlayerProvider(asset.id).select((s) => s.status)); - final position = ref.read(videoPlayerProvider(asset.id)).position; - - if (status == VideoPlaybackStatus.completed && isCurrent && position.inMicroseconds > 0) { - unawaited(_nextPage()); - } else if (status == VideoPlaybackStatus.playing) { - unawaited(ref.read(videoPlayerProvider(asset.id).notifier).setLoop(false)); - } - - return PhotoView.customChild( - onTapUp: (_, _, _) => _onTapUp(), - disableScaleGestures: true, - filterQuality: FilterQuality.high, - initialScale: scale, - child: NativeVideoViewer( - asset: asset, - isCurrent: isCurrent, - image: Image(image: imageProvider, fit: BoxFit.contain, alignment: Alignment.center), - ), + return _VideoChild( + asset: asset, + isCurrent: isCurrent, + scale: scale, + imageProvider: imageProvider, + onTapUp: _onTapUp, + onCompleted: _nextPage, ); } } @@ -411,7 +393,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si children: [ AppBar( backgroundColor: context.scaffoldBackgroundColor, - title: Text("slideshow".t(context: context)), + title: Text(context.t.slideshow), actions: [ IconButton( onPressed: _paused ? _play : _pause, @@ -477,6 +459,67 @@ class _DriftSlideshowPageState extends ConsumerState with Si } } +class _VideoChild extends ConsumerWidget { + final BaseAsset asset; + final bool isCurrent; + final PhotoViewComputedScale scale; + final ImageProvider imageProvider; + final VoidCallback onTapUp; + final VoidCallback onCompleted; + + const _VideoChild({ + required this.asset, + required this.isCurrent, + required this.scale, + required this.imageProvider, + required this.onTapUp, + required this.onCompleted, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + ref.listen(videoPlayerProvider(asset.id).select((s) => s.status), (_, status) { + if (status == VideoPlaybackStatus.completed) { + if (isCurrent && ref.read(videoPlayerProvider(asset.id)).position.inMicroseconds > 0) { + onCompleted(); + } + } else if (status == VideoPlaybackStatus.playing) { + unawaited(ref.read(videoPlayerProvider(asset.id).notifier).setLoop(false)); + } + }); + + return PhotoView.customChild( + onTapUp: (_, _, _) => onTapUp(), + disableScaleGestures: true, + filterQuality: FilterQuality.high, + initialScale: scale, + child: NativeVideoViewer( + asset: asset, + isCurrent: isCurrent, + image: Image(image: imageProvider, fit: BoxFit.contain, alignment: Alignment.center), + ), + ); + } +} + +class _VideoProgressBar extends ConsumerWidget { + final BaseAsset asset; + + const _VideoProgressBar({required this.asset}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final position = ref.watch(videoPlayerProvider(asset.id).select((s) => s.position)); + + return LinearProgressIndicator( + color: context.colorScheme.primary, + borderRadius: BorderRadius.zero, + minHeight: 5, + value: position.inMilliseconds / asset.duration.inMilliseconds, + ); + } +} + /// Progress bar for image slides, driven by an explicit [AnimationController]. /// /// [TweenAnimationBuilder] creates its controller internally with the default diff --git a/mobile/lib/presentation/pages/drift_trash.page.dart b/mobile/lib/presentation/pages/drift_trash.page.dart index 36db74a658..4dd027494d 100644 --- a/mobile/lib/presentation/pages/drift_trash.page.dart +++ b/mobile/lib/presentation/pages/drift_trash.page.dart @@ -2,7 +2,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/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart'; @@ -35,7 +34,7 @@ class DriftTrashPage extends StatelessWidget { ], child: Timeline( appBar: SliverAppBar( - title: Text('trash'.t(context: context)), + title: Text(context.t.trash), floating: true, snap: true, pinned: true, diff --git a/mobile/lib/presentation/pages/drift_user_selection.page.dart b/mobile/lib/presentation/pages/drift_user_selection.page.dart index a145b96a70..c11d5a1fd6 100644 --- a/mobile/lib/presentation/pages/drift_user_selection.page.dart +++ b/mobile/lib/presentation/pages/drift_user_selection.page.dart @@ -1,7 +1,6 @@ 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'; @@ -9,6 +8,7 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -52,6 +52,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final AsyncValue> suggestedShareUsers = ref.watch(driftUsersProvider); + final sharedUsers = ref.watch(remoteAlbumSharedUsersProvider(album.id)); final sharedUsersList = useState>({}); void addNewUsersHandler() { @@ -86,7 +87,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { Padding( padding: const EdgeInsets.all(16.0), child: Text( - 'suggestions'.tr(), + context.t.suggestions, style: const TextStyle(fontSize: 14, color: Colors.grey, fontWeight: FontWeight.bold), ), ), @@ -118,7 +119,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text('invite_to_album').tr(), + title: Text(context.t.invite_to_album), elevation: 0, centerTitle: false, leading: IconButton( @@ -130,15 +131,12 @@ class DriftUserSelectionPage extends HookConsumerWidget { actions: [ TextButton( onPressed: sharedUsersList.value.isEmpty ? null : () => addNewUsersHandler(), - child: const Text("add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), + child: Text(context.t.add, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), ), ], ), body: suggestedShareUsers.widgetWhen( onData: (users) { - // Get shared users for this album from the database - final sharedUsers = ref.read(remoteAlbumSharedUsersProvider(album.id)); - return sharedUsers.when( data: (albumSharedUsers) { // Filter out users that are already shared with this album and the owner diff --git a/mobile/lib/presentation/pages/drift_video.page.dart b/mobile/lib/presentation/pages/drift_video.page.dart index eef05acdce..b51ebc25b8 100644 --- a/mobile/lib/presentation/pages/drift_video.page.dart +++ b/mobile/lib/presentation/pages/drift_video.page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -26,7 +26,7 @@ class DriftVideoPage extends StatelessWidget { return timelineService; }), ], - child: Timeline(appBar: MesmerizingSliverAppBar(title: 'videos'.t())), + child: Timeline(appBar: MesmerizingSliverAppBar(title: context.t.videos)), ); } } diff --git a/mobile/lib/presentation/pages/edit/drift_edit.page.dart b/mobile/lib/presentation/pages/edit/drift_edit.page.dart index 0fe5d28850..6237169431 100644 --- a/mobile/lib/presentation/pages/edit/drift_edit.page.dart +++ b/mobile/lib/presentation/pages/edit/drift_edit.page.dart @@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/aspect_ratios.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/theme/theme_data.dart'; @@ -63,14 +64,14 @@ class _DriftEditImagePageState extends ConsumerState with Ti return; } - ImmichToast.show(context: context, msg: 'success'.tr(), toastType: ToastType.success); + ImmichToast.show(context: context, msg: context.t.success, toastType: ToastType.success); Navigator.of(context).pop(); } catch (e) { if (!mounted) { return; } - ImmichToast.show(context: context, msg: 'error_title'.tr(), toastType: ToastType.error); + ImmichToast.show(context: context, msg: context.t.error_title, toastType: ToastType.error); } finally { ref.read(editorStateProvider.notifier).setIsEditing(false); } @@ -80,17 +81,17 @@ class _DriftEditImagePageState extends ConsumerState with Ti return showDialog( context: context, builder: (context) => AlertDialog( - title: Text('editor_discard_edits_title'.tr()), - content: Text('editor_discard_edits_prompt'.tr()), + title: Text(context.t.editor_discard_edits_title), + content: Text(context.t.editor_discard_edits_prompt), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), style: ButtonStyle( foregroundColor: WidgetStateProperty.all(context.themeData.colorScheme.onSurfaceVariant), ), - child: Text('cancel'.tr()), + child: Text(context.t.cancel), ), - TextButton(onPressed: () => Navigator.of(context).pop(true), child: Text('confirm'.tr())), + TextButton(onPressed: () => Navigator.of(context).pop(true), child: Text(context.t.confirm)), ], ), ); @@ -116,7 +117,7 @@ class _DriftEditImagePageState extends ConsumerState with Ti child: Scaffold( appBar: AppBar( backgroundColor: Colors.black, - title: Text("edit".tr()), + title: Text(context.t.edit), leading: ImmichCloseButton(onPressed: () => Navigator.of(context).maybePop()), actions: [_SaveEditsButton(onSave: _saveEditedImage)], ), @@ -336,7 +337,7 @@ class _ResetEditsButton extends ConsumerWidget { final editorNotifier = ref.watch(editorStateProvider.notifier); return ImmichTextButton( - labelText: 'reset'.tr(), + labelText: context.t.reset, onPressed: editorNotifier.resetEdits, variant: ImmichVariant.ghost, expanded: false, diff --git a/mobile/lib/presentation/pages/feature_message/whats_new.page.dart b/mobile/lib/presentation/pages/feature_message/whats_new.page.dart index e3c3f8ed6e..fb82f59577 100644 --- a/mobile/lib/presentation/pages/feature_message/whats_new.page.dart +++ b/mobile/lib/presentation/pages/feature_message/whats_new.page.dart @@ -1,5 +1,4 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/feature_message.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -60,10 +59,10 @@ class _HighlightCard extends StatelessWidget { ), ), const SizedBox(height: 12), - Text(highlight.titleKey.tr(), style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), + Text(highlight.title(context.t), style: context.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 4), Text( - highlight.bodyKey.tr(), + highlight.body(context.t), style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant), ), ], diff --git a/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart index 523781b274..a5c069671c 100644 --- a/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart +++ b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart @@ -2,13 +2,13 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:crop_image/crop_image.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; @@ -93,7 +93,7 @@ class _ProfilePictureCropPageState extends ConsumerState ImmichToast.show( context: context, - msg: 'profile_picture_set'.tr(), + msg: context.t.profile_picture_set, gravity: ToastGravity.BOTTOM, toastType: ToastType.success, ); @@ -102,7 +102,7 @@ class _ProfilePictureCropPageState extends ConsumerState } else { ImmichToast.show( context: context, - msg: 'errors.unable_to_set_profile_picture'.tr(), + msg: context.t.errors.unable_to_set_profile_picture, toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); @@ -114,7 +114,7 @@ class _ProfilePictureCropPageState extends ConsumerState ImmichToast.show( context: context, - msg: 'errors.unable_to_set_profile_picture'.tr(), + msg: context.t.errors.unable_to_set_profile_picture, toastType: ToastType.error, gravity: ToastGravity.BOTTOM, ); @@ -135,7 +135,7 @@ class _ProfilePictureCropPageState extends ConsumerState return Scaffold( appBar: AppBar( backgroundColor: context.scaffoldBackgroundColor, - title: Text("set_profile_picture".tr()), + title: Text(context.t.set_profile_picture), leading: _isLoading ? null : const ImmichCloseButton(), actions: [ if (_isLoading) diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 12935b4f23..e364e0d2f8 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -12,7 +12,6 @@ import 'package:immich_mobile/domain/models/tag.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; @@ -48,9 +47,7 @@ class DriftSearchPage extends HookConsumerWidget { serverFeatures.smartSearch ? TextSearchType.context : TextSearchType.filename, ); final searchHintText = useState( - serverFeatures.smartSearch - ? 'sunrise_on_the_beach'.t(context: context) - : 'file_name_or_extension'.t(context: context), + serverFeatures.smartSearch ? context.t.sunrise_on_the_beach : context.t.file_name_or_extension, ); final textSearchController = useTextEditingController(); final filter = useState( @@ -59,7 +56,7 @@ class DriftSearchPage extends HookConsumerWidget { location: SearchLocationFilter(), camera: SearchCameraFilter(), date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), + display: const SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), rating: SearchRatingFilter(), mediaType: AssetType.other, language: "${context.locale.languageCode}-${context.locale.countryCode}", @@ -132,7 +129,7 @@ class DriftSearchPage extends HookConsumerWidget { void showPeoplePicker() { var people = filter.value.people; - void handleOnSelect(Set value) { + void handleOnSelect(Set value) { people = value; } @@ -142,7 +139,7 @@ class DriftSearchPage extends HookConsumerWidget { } void handleApply() { - final label = people.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', '); + final label = people.map((e) => e.name != '' ? e.name : context.t.no_name).join(', '); peopleCurrentFilterWidget.value = label.isNotEmpty ? Text(label, style: context.textTheme.labelLarge) : null; search(filter.value.copyWith(people: people)); } @@ -154,7 +151,7 @@ class DriftSearchPage extends HookConsumerWidget { child: FractionallySizedBox( heightFactor: 0.8, child: FilterBottomSheetScaffold( - title: 'search_filter_people_title'.t(context: context), + title: context.t.search_filter_people_title, expanded: true, onSearch: handleApply, onClear: handleClear, @@ -191,7 +188,7 @@ class DriftSearchPage extends HookConsumerWidget { child: FractionallySizedBox( heightFactor: 0.8, child: FilterBottomSheetScaffold( - title: 'search_filter_tags_title'.t(context: context), + title: context.t.search_filter_tags_title, expanded: true, onSearch: handleApply, onClear: handleClear, @@ -232,7 +229,7 @@ class DriftSearchPage extends HookConsumerWidget { isScrollControlled: true, isDismissible: true, child: FilterBottomSheetScaffold( - title: 'search_filter_location_title'.t(context: context), + title: context.t.search_filter_location_title, onSearch: handleApply, onClear: handleClear, child: Padding( @@ -277,7 +274,7 @@ class DriftSearchPage extends HookConsumerWidget { isScrollControlled: true, isDismissible: true, child: FilterBottomSheetScaffold( - title: 'search_filter_camera_title'.t(context: context), + title: context.t.search_filter_camera_title, onSearch: handleApply, onClear: handleClear, child: Padding( @@ -333,14 +330,14 @@ class DriftSearchPage extends HookConsumerWidget { lastDate: lastDate, currentDate: DateTime.now(), initialDateRange: dateRange, - helpText: 'search_filter_date_title'.t(context: context), - cancelText: 'cancel'.t(context: context), - confirmText: 'select'.t(context: context), - saveText: 'save'.t(context: context), - errorFormatText: 'invalid_date_format'.t(context: context), - errorInvalidText: 'invalid_date'.t(context: context), - fieldStartHintText: 'start_date'.t(context: context), - fieldEndHintText: 'end_date'.t(context: context), + helpText: context.t.search_filter_date_title, + cancelText: context.t.cancel, + confirmText: context.t.select, + saveText: context.t.save, + errorFormatText: context.t.invalid_date_format, + errorInvalidText: context.t.invalid_date, + fieldStartHintText: context.t.start_date, + fieldEndHintText: context.t.end_date, initialEntryMode: DatePickerEntryMode.calendar, keyboardType: TextInputType.text, ); @@ -357,7 +354,7 @@ class DriftSearchPage extends HookConsumerWidget { showFilterBottomSheet( context: context, child: FilterBottomSheetScaffold( - title: "pick_date_range".tr(), + title: context.t.pick_date_range, expanded: true, onClear: () => datePicked(null), child: QuickDatePicker( @@ -392,7 +389,7 @@ class DriftSearchPage extends HookConsumerWidget { void handleApply() { mediaTypeCurrentFilterWidget.value = mediaType != AssetType.other ? Text( - mediaType == AssetType.image ? 'image'.t(context: context) : 'video'.t(context: context), + mediaType == AssetType.image ? context.t.image : context.t.video, style: context.textTheme.labelLarge, ) : null; @@ -403,7 +400,7 @@ class DriftSearchPage extends HookConsumerWidget { showFilterBottomSheet( context: context, child: FilterBottomSheetScaffold( - title: 'search_filter_media_type_title'.t(context: context), + title: context.t.search_filter_media_type_title, onSearch: handleApply, onClear: handleClear, child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType), @@ -427,10 +424,7 @@ class DriftSearchPage extends HookConsumerWidget { void handleApply() { ratingCurrentFilterWidget.value = rating.rating.isSome - ? Text( - 'rating_count'.t(args: {'count': rating.rating.unwrapOrNull ?? 0}), - style: context.textTheme.labelLarge, - ) + ? Text(context.t.rating_count(count: rating.rating.unwrapOrNull ?? 0), style: context.textTheme.labelLarge) : null; search(filter.value.copyWith(rating: rating)); } @@ -440,7 +434,7 @@ class DriftSearchPage extends HookConsumerWidget { context: context, isScrollControlled: true, child: FilterBottomSheetScaffold( - title: 'rating'.t(context: context), + title: context.t.rating, onSearch: handleApply, onClear: handleClear, child: StarRatingPicker(onSelect: handleOnSelected, filter: filter.value.rating), @@ -455,9 +449,9 @@ class DriftSearchPage extends HookConsumerWidget { void handleOnSelect(Map value) { display = display.copyWith( - isNotInAlbum: value[DisplayOption.notInAlbum], - isArchive: value[DisplayOption.archive], - isFavorite: value[DisplayOption.favorite], + isNotInAlbum: value[DisplayOption.notInAlbum] ?? display.isNotInAlbum, + isArchive: value[DisplayOption.archive] ?? display.isArchive, + isFavorite: value[DisplayOption.favorite] ?? display.isFavorite, ); } @@ -465,16 +459,16 @@ class DriftSearchPage extends HookConsumerWidget { displayOptionCurrentFilterWidget.value = null; search( filter.value.copyWith( - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), + display: const SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), ), ); } void handleApply() { final filterText = [ - if (display.isNotInAlbum) 'search_filter_display_option_not_in_album'.t(context: context), - if (display.isArchive) 'archive'.t(context: context), - if (display.isFavorite) 'favorite'.t(context: context), + if (display.isNotInAlbum) context.t.search_filter_display_option_not_in_album, + if (display.isArchive) context.t.archive, + if (display.isFavorite) context.t.favorite, ]; displayOptionCurrentFilterWidget.value = filterText.isNotEmpty ? Text(filterText.join(', '), style: context.textTheme.labelLarge) @@ -486,7 +480,7 @@ class DriftSearchPage extends HookConsumerWidget { showFilterBottomSheet( context: context, child: FilterBottomSheetScaffold( - title: 'display_options'.t(context: context), + title: context.t.display_options, onSearch: handleApply, onClear: handleClear, child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display), @@ -534,7 +528,7 @@ class DriftSearchPage extends HookConsumerWidget { } }, icon: const Icon(Icons.more_vert_rounded), - tooltip: 'show_text_search_menu'.tr(), + tooltip: context.t.show_text_search_menu, ); }, menuChildren: [ @@ -544,7 +538,7 @@ class DriftSearchPage extends HookConsumerWidget { child: ListTile( leading: const Icon(Icons.image_search_rounded), title: Text( - 'search_by_context'.t(context: context), + context.t.search_by_context, style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, color: textSearchType.value == TextSearchType.context ? context.colorScheme.primary : null, @@ -555,7 +549,7 @@ class DriftSearchPage extends HookConsumerWidget { ), onPressed: () { textSearchType.value = TextSearchType.context; - searchHintText.value = 'sunrise_on_the_beach'.t(context: context); + searchHintText.value = context.t.sunrise_on_the_beach; }, ), ), @@ -563,7 +557,7 @@ class DriftSearchPage extends HookConsumerWidget { child: ListTile( leading: const Icon(Icons.abc_rounded), title: Text( - 'search_filter_filename'.t(context: context), + context.t.search_filter_filename, style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, color: textSearchType.value == TextSearchType.filename ? context.colorScheme.primary : null, @@ -574,14 +568,14 @@ class DriftSearchPage extends HookConsumerWidget { ), onPressed: () { textSearchType.value = TextSearchType.filename; - searchHintText.value = 'file_name_or_extension'.t(context: context); + searchHintText.value = context.t.file_name_or_extension; }, ), MenuItemButton( child: ListTile( leading: const Icon(Icons.text_snippet_outlined), title: Text( - 'search_by_description'.t(context: context), + context.t.search_by_description, style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, color: textSearchType.value == TextSearchType.description ? context.colorScheme.primary : null, @@ -592,7 +586,7 @@ class DriftSearchPage extends HookConsumerWidget { ), onPressed: () { textSearchType.value = TextSearchType.description; - searchHintText.value = 'search_by_description_example'.t(context: context); + searchHintText.value = context.t.search_by_description_example; }, ), FeatureCheck( @@ -601,7 +595,7 @@ class DriftSearchPage extends HookConsumerWidget { child: ListTile( leading: const Icon(Icons.document_scanner_outlined), title: Text( - 'search_by_ocr'.t(context: context), + context.t.search_by_ocr, style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, color: textSearchType.value == TextSearchType.ocr ? context.colorScheme.primary : null, @@ -612,7 +606,7 @@ class DriftSearchPage extends HookConsumerWidget { ), onPressed: () { textSearchType.value = TextSearchType.ocr; - searchHintText.value = 'search_by_ocr_example'.t(context: context); + searchHintText.value = context.t.search_by_ocr_example; }, ), ), @@ -663,52 +657,52 @@ class DriftSearchPage extends HookConsumerWidget { SearchFilterChip( icon: Icons.people_alt_outlined, onTap: showPeoplePicker, - label: 'people'.t(context: context), + label: context.t.people, currentFilter: peopleCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.location_on_outlined, onTap: showLocationPicker, - label: 'search_filter_location'.t(context: context), + label: context.t.search_filter_location, currentFilter: locationCurrentFilterWidget.value, ), if (userPreferences.valueOrNull?.tagsEnabled ?? false) SearchFilterChip( icon: Icons.sell_outlined, onTap: showTagPicker, - label: 'tags'.t(context: context), + label: context.t.tags, currentFilter: tagCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.camera_alt_outlined, onTap: showCameraPicker, - label: 'camera'.t(context: context), + label: context.t.camera, currentFilter: cameraCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.date_range_outlined, onTap: showQuickDatePicker, - label: 'search_filter_date'.t(context: context), + label: context.t.search_filter_date, currentFilter: dateRangeCurrentFilterWidget.value, ), SearchFilterChip( key: const Key('media_type_chip'), icon: Icons.video_collection_outlined, onTap: showMediaTypePicker, - label: 'search_filter_media_type'.t(context: context), + label: context.t.search_filter_media_type, currentFilter: mediaTypeCurrentFilterWidget.value, ), if (userPreferences.valueOrNull?.ratingsEnabled ?? false) SearchFilterChip( icon: Icons.star_outline_rounded, onTap: showStarRatingPicker, - label: 'search_filter_star_rating'.t(context: context), + label: context.t.search_filter_star_rating, currentFilter: ratingCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.display_settings_outlined, onTap: showDisplayOptionPicker, - label: 'search_filter_display_options'.t(context: context), + label: context.t.search_filter_display_options, currentFilter: displayOptionCurrentFilterWidget.value, ), ], @@ -748,9 +742,7 @@ class _SearchResultGrid extends ConsumerWidget { return false; } - Widget? _bottomWidget(BuildContext context, WidgetRef ref) { - final isLoading = ref.read(paginatedSearchProvider.select((s) => s.isLoading)); - + Widget? _bottomWidget(BuildContext context, {required bool isLoading, required bool hasMore}) { if (isLoading) { return const SliverFillRemaining( hasScrollBody: false, @@ -761,8 +753,6 @@ class _SearchResultGrid extends ConsumerWidget { ); } - final hasMore = ref.read(paginatedSearchProvider.select((s) => s.nextPage != null)); - if (hasMore) { return null; } @@ -772,7 +762,7 @@ class _SearchResultGrid extends ConsumerWidget { padding: const EdgeInsets.symmetric(vertical: 32), child: Center( child: Text( - 'search_no_more_result'.t(context: context), + context.t.search_no_more_result, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceVariant), ), ), @@ -784,6 +774,7 @@ class _SearchResultGrid extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final hasAssets = ref.watch(paginatedSearchProvider.select((s) => s.assets.isNotEmpty)); final isLoading = ref.watch(paginatedSearchProvider.select((s) => s.isLoading)); + final hasMore = ref.watch(paginatedSearchProvider.select((s) => s.nextPage != null)); if (!hasAssets && !isLoading) { return const _SearchNoResults(); @@ -813,7 +804,7 @@ class _SearchResultGrid extends ConsumerWidget { bottomSheet: const GeneralBottomSheet(minChildSize: 0.20), snapToMonth: false, loadingWidget: const SizedBox.shrink(), - bottomSliverWidget: _bottomWidget(context, ref), + bottomSliverWidget: _bottomWidget(context, isLoading: isLoading, hasMore: hasMore), ), ), ), @@ -837,7 +828,7 @@ class _SearchNoResults extends StatelessWidget { Icon(Icons.search_off_rounded, size: 72, color: context.colorScheme.onSurfaceVariant), const SizedBox(height: 24), Text( - 'search_no_result'.t(context: context), + context.t.search_no_result, textAlign: TextAlign.center, style: context.textTheme.bodyLarge?.copyWith(color: context.colorScheme.onSurfaceVariant), ), @@ -865,9 +856,7 @@ class _SearchSuggestions extends StatelessWidget { ), ), const SizedBox(height: 16), - Center( - child: Text('search_page_search_photos_videos'.t(context: context), style: context.textTheme.labelLarge), - ), + Center(child: Text(context.t.search_page_search_photos_videos, style: context.textTheme.labelLarge)), const SizedBox(height: 32), const Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: _QuickLinkList()), ], @@ -900,7 +889,7 @@ class _QuickLinkList extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), children: [ _QuickLink( - title: 'recently_taken'.t(context: context), + title: context.t.recently_taken, icon: Icons.schedule_outlined, isTop: true, onTap: () => context.pushRoute(const DriftRecentlyTakenRoute()), @@ -911,12 +900,12 @@ class _QuickLinkList extends StatelessWidget { onTap: () => context.pushRoute(const DriftRecentlyAddedRoute()), ), _QuickLink( - title: 'videos'.t(context: context), + title: context.t.videos, icon: Icons.play_circle_outline_rounded, onTap: () => context.pushRoute(const DriftVideoRoute()), ), _QuickLink( - title: 'favorites'.t(context: context), + title: context.t.favorites, icon: Icons.favorite_border_rounded, isBottom: true, onTap: () => context.pushRoute(const DriftFavoriteRoute()), diff --git a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart index cfb5887c7c..2466a52ead 100644 --- a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart @@ -1,13 +1,12 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; @@ -52,11 +51,11 @@ class _AddActionButtonState extends ConsumerState { return [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text("add_to_bottom_bar".tr(), style: context.textTheme.labelMedium), + child: Text(context.t.add_to_bottom_bar, style: context.textTheme.labelMedium), ), BaseActionButton( iconData: Icons.photo_album_outlined, - label: "album".tr(), + label: context.t.album, menuItem: true, onPressed: () => _handleMenuSelection(AddToMenuItem.album), ), @@ -65,7 +64,7 @@ class _AddActionButtonState extends ConsumerState { const Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Text("move_to".tr(), style: context.textTheme.labelMedium), + child: Text(context.t.move_to, style: context.textTheme.labelMedium), ), const ActionMenuItem(action: ArchiveAction(source: .viewer)), const ActionMenuItem(action: LockAction(source: .viewer)), @@ -120,7 +119,7 @@ class _AddActionButtonState extends ConsumerState { } if (!result.success) { - ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + ImmichToast.show(context: context, msg: context.t.scaffold_body_error_occurred, toastType: ToastType.error); return; } @@ -128,7 +127,7 @@ class _AddActionButtonState extends ConsumerState { if (result.count > 0) { ImmichToast.show( context: context, - msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + msg: context.t.add_to_album_bottom_sheet_added(album: album.name), ); // Refresh the "Appears in" list on the asset's info panel. @@ -136,13 +135,13 @@ class _AddActionButtonState extends ConsumerState { } else if (result.failedCount > 0) { ImmichToast.show( context: context, - msg: 'assets_cannot_be_added_to_album_count'.t(context: context, args: {'count': result.failedCount}), + msg: context.t.assets_cannot_be_added_to_album_count(count: result.failedCount), toastType: ToastType.error, ); } else { ImmichToast.show( context: context, - msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}), + msg: context.t.add_to_album_bottom_sheet_already_exists(album: album.name), ); } @@ -183,7 +182,7 @@ class _AddActionButtonState extends ConsumerState { builder: (context, controller, child) { return BaseActionButton( iconData: Icons.add, - label: "add_to_bottom_bar".tr(), + label: context.t.add_to_bottom_bar, onPressed: () => controller.isOpen ? controller.close() : controller.open(), ); }, diff --git a/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart index 46a5c2c41e..e39a76ef02 100644 --- a/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart @@ -1,9 +1,8 @@ import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/activities/activity.model.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/activity.provider.dart'; @@ -48,7 +47,7 @@ class LikeActivityActionButton extends ConsumerWidget { return BaseActionButton( maxWidth: 60, iconData: liked != null ? Icons.thumb_up : Icons.thumb_up_off_alt, - label: "like".t(context: context), + label: context.t.like, onPressed: () => onTap(liked), iconOnly: iconOnly, menuItem: menuItem, @@ -58,11 +57,11 @@ class LikeActivityActionButton extends ConsumerWidget { // default to empty heart during loading loading: () => BaseActionButton( iconData: Icons.thumb_up_off_alt, - label: "like".t(context: context), + label: context.t.like, iconOnly: iconOnly, menuItem: menuItem, ), - error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])), + error: (error, stack) => Text(context.t.error_saving_image(error: error.toString())), ); } } diff --git a/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart index 878c589b21..6be8a6b023 100644 --- a/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; @@ -16,7 +16,7 @@ class MotionPhotoActionButton extends ConsumerWidget { return BaseActionButton( iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded, - label: "play_motion_photo".t(context: context), + label: context.t.play_motion_photo, onPressed: ref.watch(isPlayingMotionVideoProvider.notifier).toggle, iconOnly: iconOnly, menuItem: menuItem, diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 9babdb5b57..521f7d0f77 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -2,14 +2,13 @@ import 'dart:async'; import 'dart:math'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart'; @@ -326,7 +325,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { ), ), child: Text( - sortMode.label.t(context: context), + sortMode.label(context.t), style: context.textTheme.labelLarge?.copyWith( color: albumSortOption == sortMode ? context.colorScheme.onPrimary @@ -354,7 +353,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { : Icon(Icons.keyboard_arrow_up_rounded, color: context.colorScheme.onSurface), ), Text( - albumSortOption.label.t(context: context), + albumSortOption.label(context.t), style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(225)), ), isSorting @@ -413,7 +412,7 @@ class _SearchBar extends StatelessWidget { child: SearchField( autofocus: false, contentPadding: const EdgeInsets.all(16), - hintText: 'search_albums'.tr(), + hintText: context.t.search_albums, prefixIcon: const Icon(Icons.search_rounded), suffixIcon: searchController.text.isNotEmpty ? IconButton(icon: const Icon(Icons.clear_rounded), onPressed: onClearSearch) @@ -452,7 +451,7 @@ class _QuickFilterButtonRow extends StatelessWidget { runSpacing: 4, children: [ _QuickFilterButton( - label: 'all'.tr(), + label: context.t.all, isSelected: filterMode == QuickFilterMode.all, onTap: () { onChangeFilter(QuickFilterMode.all); @@ -460,7 +459,7 @@ class _QuickFilterButtonRow extends StatelessWidget { }, ), _QuickFilterButton( - label: 'shared_with_me'.tr(), + label: context.t.shared_with_me, isSelected: filterMode == QuickFilterMode.sharedWithMe, onTap: () { onChangeFilter(QuickFilterMode.sharedWithMe); @@ -468,7 +467,7 @@ class _QuickFilterButtonRow extends StatelessWidget { }, ), _QuickFilterButton( - label: 'my_albums'.tr(), + label: context.t.my_albums, isSelected: filterMode == QuickFilterMode.myAlbums, onTap: () { onChangeFilter(QuickFilterMode.myAlbums); @@ -571,7 +570,7 @@ class _AlbumList extends ConsumerWidget { if (albums.isEmpty) { return SliverToBoxAdapter( child: Center( - child: Padding(padding: const EdgeInsets.all(20.0), child: Text('album_search_not_found'.tr())), + child: Padding(padding: const EdgeInsets.all(20.0), child: Text(context.t.album_search_not_found)), ), ); } @@ -600,9 +599,9 @@ class _AlbumList extends ConsumerWidget { context: context, builder: (context) => ConfirmDialog( onOk: () => true, - title: "delete_album".t(context: context), - content: "album_delete_confirmation".t(context: context, args: {'album': album.name}), - ok: "delete".t(context: context), + title: context.t.delete_album, + content: context.t.album_delete_confirmation(album: album.name), + ok: context.t.delete, ), ); }, @@ -637,7 +636,7 @@ class _AlbumGrid extends StatelessWidget { if (albums.isEmpty) { return SliverToBoxAdapter( child: Center( - child: Padding(padding: const EdgeInsets.all(20.0), child: Text('album_search_not_found'.tr())), + child: Padding(padding: const EdgeInsets.all(20.0), child: Text(context.t.album_search_not_found)), ), ); } @@ -723,7 +722,7 @@ class _GridAlbumCard extends ConsumerWidget { style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), ), Text( - '${'items_count'.t(context: context, args: {'count': album.assetCount})} â€ĸ ${album.ownerId != userId ? 'shared_by_user'.t(context: context, args: {'user': album.ownerName}) : 'owned'.t(context: context)}', + '${context.t.items_count(count: album.assetCount)} â€ĸ ${album.ownerId != userId ? context.t.shared_by_user(user: album.ownerName) : context.t.owned}', maxLines: 1, overflow: TextOverflow.ellipsis, style: context.textTheme.labelMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), @@ -760,7 +759,7 @@ class AddToAlbumHeader extends ConsumerWidget { } if (newAlbum == null) { - ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr()); + ImmichToast.show(context: context, toastType: ToastType.error, msg: context.t.errors.failed_to_create_album); return; } @@ -774,7 +773,7 @@ class AddToAlbumHeader extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("add_to_album", style: context.textTheme.titleSmall).tr(), + Text(context.t.add_to_album, style: context.textTheme.titleSmall), TextButton.icon( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), // remove internal padding @@ -784,9 +783,9 @@ class AddToAlbumHeader extends ConsumerWidget { onPressed: onCreateAlbum, icon: Icon(Icons.add, color: context.primaryColor), label: Text( - "common_create_new_album", + context.t.common_create_new_album, style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14), - ).tr(), + ), ), ], ), @@ -822,13 +821,13 @@ class CreateAlbumButton extends ConsumerWidget { } if (album == null) { - ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr()); + ImmichToast.show(context: context, toastType: ToastType.error, msg: context.t.errors.failed_to_create_album); return; } ImmichToast.show( context: context, - msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + msg: context.t.add_to_album_bottom_sheet_added(album: album.name), ); // Invalidate using the asset's remote ID to refresh the "Appears in" list @@ -843,7 +842,7 @@ class CreateAlbumButton extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("add_to_album", style: context.textTheme.titleSmall).tr(), + Text(context.t.add_to_album, style: context.textTheme.titleSmall), TextButton.icon( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), @@ -853,9 +852,9 @@ class CreateAlbumButton extends ConsumerWidget { onPressed: onCreateAlbum, icon: Icon(Icons.add, color: context.primaryColor), label: Text( - "common_create_new_album", + context.t.common_create_new_album, style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14), - ).tr(), + ), ), ], ), diff --git a/mobile/lib/presentation/widgets/album/album_tile.dart b/mobile/lib/presentation/widgets/album/album_tile.dart index 598666cda9..54fc27cfd1 100644 --- a/mobile/lib/presentation/widgets/album/album_tile.dart +++ b/mobile/lib/presentation/widgets/album/album_tile.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; @@ -27,7 +27,7 @@ class AlbumTile extends ConsumerWidget { style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), ), subtitle: Text( - '${'items_count'.t(context: context, args: {'count': album.assetCount})} â€ĸ ${isOwner ? 'owned'.t(context: context) : 'shared_by_user'.t(context: context, args: {'user': album.ownerName})}', + '${context.t.items_count(count: album.assetCount)} â€ĸ ${isOwner ? context.t.owned : context.t.shared_by_user(user: album.ownerName)}', overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), diff --git a/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart b/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart index 691b46f80d..55024d9b2b 100644 --- a/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart +++ b/mobile/lib/presentation/widgets/album/drift_activity_text_field.dart @@ -1,7 +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/generated/translations.g.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @@ -98,7 +98,7 @@ class _DriftActivityTextFieldState extends ConsumerState color: context.primaryColor, disabledColor: context.colorScheme.secondaryContainer, ), - hintText: !widget.isEnabled ? 'shared_album_activities_input_disable'.tr() : 'say_something'.tr(), + hintText: !widget.isEnabled ? context.t.shared_album_activities_input_disable : context.t.say_something, hintStyle: TextStyle(fontWeight: FontWeight.normal, fontSize: 14, color: Colors.grey[600]), ), onEditingComplete: onEditingComplete, diff --git a/mobile/lib/presentation/widgets/album/new_album_name_modal.widget.dart b/mobile/lib/presentation/widgets/album/new_album_name_modal.widget.dart index a5e21af489..255dbd4a0c 100644 --- a/mobile/lib/presentation/widgets/album/new_album_name_modal.widget.dart +++ b/mobile/lib/presentation/widgets/album/new_album_name_modal.widget.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class NewAlbumNameModal extends StatefulWidget { const NewAlbumNameModal({super.key}); @@ -21,31 +21,31 @@ class _NewAlbumNameModalState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: const Text("album_name", style: TextStyle(fontWeight: FontWeight.bold)).tr(), + title: Text(context.t.album_name, style: const TextStyle(fontWeight: FontWeight.bold)), content: SingleChildScrollView( child: TextFormField( controller: nameController, textCapitalization: TextCapitalization.words, autofocus: true, - decoration: InputDecoration(hintText: 'name'.tr(), border: const OutlineInputBorder()), + decoration: InputDecoration(hintText: context.t.name, border: const OutlineInputBorder()), ), ), actions: [ TextButton( onPressed: () => context.pop(null), child: Text( - "cancel", + context.t.cancel, style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold), - ).tr(), + ), ), TextButton( onPressed: () { context.pop(nameController.text.trim()); }, child: Text( - "create_album", + context.t.create_album, style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), + ), ), ], ); diff --git a/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart b/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart index 5622ed6b1b..c281f6b166 100644 --- a/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart +++ b/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart'; import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; @@ -115,7 +115,7 @@ class _PendingUploadsBannerContent extends StatelessWidget { const SizedBox(width: 12), Expanded( child: Text( - '${'uploading'.t(context: context)} $count$percentLabel', + '${context.t.uploading} $count$percentLabel', maxLines: 1, overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), @@ -183,10 +183,7 @@ class _PendingUploadsSheet extends ConsumerWidget { child: Row( children: [ Expanded( - child: Text( - '${'uploading'.t(context: context)} (${pending.length})', - style: context.textTheme.titleMedium, - ), + child: Text('${context.t.uploading} (${pending.length})', style: context.textTheme.titleMedium), ), if (canAbort) TextButton.icon( @@ -199,14 +196,14 @@ class _PendingUploadsSheet extends ConsumerWidget { ref.read(pendingAlbumUploadsProvider(albumId).notifier).clear(); }, icon: const Icon(Icons.stop_circle_outlined, size: 18), - label: Text('cancel'.t(context: context)), + label: Text(context.t.cancel), style: TextButton.styleFrom(foregroundColor: context.colorScheme.error), ) else if (failedCount > 0) TextButton.icon( onPressed: () => ref.read(pendingAlbumUploadsProvider(albumId).notifier).clearFailed(), icon: const Icon(Icons.clear_rounded, size: 18), - label: Text('clear_failed_count'.t(context: context, args: {'count': failedCount})), + label: Text(context.t.clear_failed_count(count: failedCount)), style: TextButton.styleFrom(foregroundColor: context.colorScheme.error), ), ], diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart index dffda47b84..efd2ee00a1 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart @@ -7,7 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; @@ -52,7 +52,7 @@ class AppearsInDetails extends ConsumerWidget { spacing: 12, children: [ SheetTile( - title: 'appears_in'.t(context: context), + title: context.t.appears_in, titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), Padding( diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart index eff5705eae..9f426f6db7 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart @@ -8,7 +8,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -100,7 +100,7 @@ class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription> ImmichToast.show( context: context, - msg: 'exif_bottom_sheet_description_error'.t(context: context), + msg: context.t.exif_bottom_sheet_description_error, toastType: ToastType.error, ); } @@ -111,9 +111,9 @@ class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription> @override Widget build(BuildContext context) { final currentDescription = widget.exif.description ?? ''; - final hintText = (widget.isEditable ? 'exif_bottom_sheet_description' : 'exif_bottom_sheet_no_description').t( - context: context, - ); + final hintText = widget.isEditable + ? context.t.exif_bottom_sheet_description + : context.t.exif_bottom_sheet_no_description; if (_controller.text != currentDescription && !_descriptionFocus.hasFocus) { _controller.text = currentDescription; } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart index c6d048e458..47ecb53d8a 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart @@ -6,7 +6,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; @@ -75,7 +75,7 @@ class _LocationDetailsState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ SheetTile( - title: 'location'.t(context: context), + title: context.t.location, titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), trailing: hasCoordinates && editLocation != null ? const Icon(Icons.edit_location_alt, size: 20) : null, onTap: editLocation?.onAction, @@ -107,7 +107,7 @@ class _LocationDetailsState extends ConsumerState { ), if (!hasCoordinates) SheetTile( - title: "add_a_location".t(context: context), + title: context.t.add_a_location, titleStyle: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: context.primaryColor, diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart index 72db236b3c..00c20f35bd 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/people/person_edit_name_modal.widget.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; @@ -30,7 +30,7 @@ class PeopleDetails extends ConsumerWidget { final peopleFuture = ref.watch(driftPeopleAssetProvider(asset.id)); - Future showNameEditModal(DriftPerson person) async { + Future showNameEditModal(Person person) async { await showDialog( context: context, useRootNavigator: false, @@ -52,7 +52,7 @@ class PeopleDetails extends ConsumerWidget { Padding( padding: const EdgeInsets.only(left: 16, top: 16, bottom: 16), child: Text( - "people".t(context: context), + context.t.people, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ), @@ -89,14 +89,14 @@ class PeopleDetails extends ConsumerWidget { duration: Durations.short4, ); }, - error: (error, stack) => Text("error_loading_people".t(context: context), style: context.textTheme.bodyMedium), + error: (error, stack) => Text(context.t.errors.failed_to_load_people, style: context.textTheme.bodyMedium), loading: () => const SizedBox.shrink(), ); } } class _Avatar extends StatelessWidget { - final DriftPerson person; + final Person person; final DateTime assetFileCreatedAt; final VoidCallback? onTap; final VoidCallback? onNameTap; @@ -135,7 +135,7 @@ class _Avatar extends StatelessWidget { GestureDetector( onTap: () => onNameTap?.call(), child: Text( - "add_a_name".t(context: context), + context.t.add_a_name, style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), maxLines: 2, overflow: TextOverflow.ellipsis, diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart index 352838c761..f07fb5c112 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/rating_bar.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; @@ -32,7 +32,7 @@ class RatingDetails extends ConsumerWidget { spacing: 8, children: [ Text( - 'rating'.t(context: context), + context.t.rating, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), RatingBar( diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart index e92b671562..e36cd9967b 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; @@ -27,7 +27,7 @@ class TechnicalDetails extends ConsumerWidget { return Column( children: [ SheetTile( - title: 'details'.t(context: context), + title: context.t.details, titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), _buildFileInfoTile(context, ref, asset, exifInfo), diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart index c887f8e65a..7556505158 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -333,9 +333,15 @@ class _AssetPageState extends ConsumerState { required bool isCurrent, required bool isPlayingMotionVideo, required String? localFilePath, + required Size? remoteThumbnailSize, }) { final size = context.sizeData; - final imageProvider = getFullImageProvider(asset, size: size, localFilePath: localFilePath); + final imageProvider = getFullImageProvider( + asset, + size: size, + localFilePath: localFilePath, + remoteThumbnailSize: remoteThumbnailSize, + ); if (asset.isImage && !isPlayingMotionVideo) { return PhotoView( @@ -397,7 +403,9 @@ class _AssetPageState extends ConsumerState { @override Widget build(BuildContext context) { - final currentAsset = ref.watch(assetViewerProvider.select((s) => s.currentAsset)); + final (currentAsset, thumbnailSize) = ref.watch( + assetViewerProvider.select((s) => (s.currentAsset, s.thumbnailSize)), + ); _showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); final stackIndex = ref.watch(assetViewerProvider.select((s) => s.stackIndex)); final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider); @@ -454,6 +462,7 @@ class _AssetPageState extends ConsumerState { isCurrent: isCurrent, isPlayingMotionVideo: isPlayingMotionVideo, localFilePath: viewIntentFilePath, + remoteThumbnailSize: thumbnailSize, ), ), if (showingOcr && displayAsset.width != null && displayAsset.height != null) diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_preloader.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_preloader.dart index 4d1856b90d..8c153e9995 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_preloader.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_preloader.dart @@ -17,7 +17,8 @@ class AssetPreloader { AssetPreloader({required this.timelineService, required this.mounted}); - void preload(int index, Size size) { + /// Preloads adjacent images with the current thumbnail size. + void preload(int index, Size size, {Size? thumbnailSize}) { unawaited(timelineService.preloadAssets(index)); _timer?.cancel(); _timer = Timer(Durations.medium4, () async { @@ -33,13 +34,14 @@ class AssetPreloader { } _prevStream?.removeListener(_dummyListener); _nextStream?.removeListener(_dummyListener); - _prevStream = prev != null ? _resolveImage(prev, size) : null; - _nextStream = next != null ? _resolveImage(next, size) : null; + _prevStream = prev != null ? _resolveImage(prev, size, thumbnailSize) : null; + _nextStream = next != null ? _resolveImage(next, size, thumbnailSize) : null; }); } - ImageStream _resolveImage(BaseAsset asset, Size size) { - return getFullImageProvider(asset, size: size).resolve(ImageConfiguration.empty)..addListener(_dummyListener); + ImageStream _resolveImage(BaseAsset asset, Size size, Size? thumbnailSize) { + return getFullImageProvider(asset, size: size, remoteThumbnailSize: thumbnailSize).resolve(ImageConfiguration.empty) + ..addListener(_dummyListener); } void dispose() { diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index 23cc6119fb..ad330309ab 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -2,7 +2,6 @@ import 'dart:async'; 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:hooks_riverpod/hooks_riverpod.dart'; @@ -14,6 +13,7 @@ import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/scroll_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_status_floating_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_page.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_preloader.dart'; @@ -65,7 +65,8 @@ class AssetViewer extends ConsumerStatefulWidget { @override ConsumerState createState() => _AssetViewerState(); - static void setAsset(WidgetRef ref, BaseAsset asset) { + /// Sets the asset and thumbnail size before opening the viewer. + static void setAsset(WidgetRef ref, BaseAsset asset, {Size? thumbnailSize}) { ref.read(assetViewerProvider.notifier).reset(); // Hide controls by default for videos @@ -73,11 +74,7 @@ class AssetViewer extends ConsumerStatefulWidget { ref.read(assetViewerProvider.notifier).setControls(false); } - _setAsset(ref, asset); - } - - static void _setAsset(WidgetRef ref, BaseAsset asset) { - ref.read(assetViewerProvider.notifier).setAsset(asset); + ref.read(assetViewerProvider.notifier).setAsset(asset, thumbnailSize: thumbnailSize); } } @@ -163,7 +160,11 @@ class _AssetViewerState extends ConsumerState { } void _onAssetInit(Duration timeStamp) { - _preloader.preload(widget.initialIndex, context.sizeData); + _preloader.preload( + widget.initialIndex, + context.sizeData, + thumbnailSize: ref.read(assetViewerProvider).thumbnailSize, + ); _handleCasting(); } @@ -181,8 +182,8 @@ class _AssetViewerState extends ConsumerState { return; } - AssetViewer._setAsset(ref, asset); - _preloader.preload(index, context.sizeData); + ref.read(assetViewerProvider.notifier).setAsset(asset); + _preloader.preload(index, context.sizeData, thumbnailSize: ref.read(assetViewerProvider).thumbnailSize); _handleCasting(); _stackChildrenKeepAlive?.close(); _stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive(); @@ -209,7 +210,7 @@ class _AssetViewerState extends ConsumerState { SnackBar( duration: const Duration(seconds: 2), content: Text( - "local_asset_cast_failed".tr(), + context.t.local_asset_cast_failed, style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/motion_photo_button.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/motion_photo_button.widget.dart index 08d6b59423..78f234d39b 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/motion_photo_button.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/motion_photo_button.widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; @@ -66,7 +66,7 @@ class _MotionButton extends StatelessWidget { ), const SizedBox(width: 8), Text( - CurrentPlatform.isAndroid ? 'motion'.t(context: context) : 'live'.t(context: context), + CurrentPlatform.isAndroid ? context.t.motion : context.t.live, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), ], diff --git a/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart index 0ed37869be..fb5cfff255 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class RatingBar extends StatefulWidget { final double initialRating; @@ -129,10 +129,7 @@ class _RatingBarState extends State { }); widget.onClearRating?.call(); }, - child: Text( - 'rating_clear'.t(context: context), - style: TextStyle(color: context.themeData.colorScheme.primary), - ), + child: Text(context.t.rating_clear, style: TextStyle(color: context.themeData.colorScheme.primary)), ), ), ], diff --git a/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart index ac6f79a155..8ff0befd7b 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -29,11 +29,7 @@ class SheetTile extends ConsumerWidget { void copyTitle(BuildContext context, WidgetRef ref) { unawaited(Clipboard.setData(ClipboardData(text: title))); - ImmichToast.show( - context: context, - msg: 'copied_to_clipboard'.t(context: context), - toastType: ToastType.info, - ); + ImmichToast.show(context: context, msg: context.t.copied_to_clipboard, toastType: ToastType.info); ref.read(hapticFeedbackProvider.notifier).selectionClick(); } diff --git a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart index b424298f5b..383ec17952 100644 --- a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart +++ b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; @@ -135,7 +135,7 @@ class BackupToggleButtonState extends ConsumerState with Sin children: [ Flexible( child: Text( - "enable_backup".t(context: context), + context.t.enable_backup, style: context.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: context.primaryColor, @@ -148,7 +148,7 @@ class BackupToggleButtonState extends ConsumerState with Sin Padding( padding: const EdgeInsets.only(top: 2), child: Text( - "upload_error_with_count".t(context: context, args: {'count': '$errorCount'}), + context.t.upload_error_with_count(count: errorCount), style: context.textTheme.labelMedium?.copyWith(color: context.colorScheme.error), ), ), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index 837397254c..c68ce8d9ca 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -1,8 +1,8 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; @@ -52,15 +52,15 @@ class _ArchiveBottomSheetState extends ConsumerState { } if (!result.success) { - ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + ImmichToast.show(context: context, msg: context.t.scaffold_body_error_occurred, toastType: ToastType.error); return; } ImmichToast.show( context: context, msg: result.count == 0 - ? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}) - : 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + ? context.t.add_to_album_bottom_sheet_already_exists(album: album.name) + : context.t.add_to_album_bottom_sheet_added(album: album.name), ); } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index 3322035320..5ac6869c57 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; @@ -43,28 +43,25 @@ class FavoriteBottomSheet extends ConsumerWidget { } if (selectedAssets.length != remoteAssets.length) { - ImmichToast.show( - context: context, - msg: 'add_to_album_bottom_sheet_some_local_assets'.t(context: context), - ); + ImmichToast.show(context: context, msg: context.t.add_to_album_bottom_sheet_some_local_assets); } // Only report the failure when nothing was added; if some succeeded we show "added". if (result.added > 0) { ImmichToast.show( context: context, - msg: 'add_to_album_bottom_sheet_added'.t(args: {"album": album.name}), + msg: context.t.add_to_album_bottom_sheet_added(album: album.name), ); } else if (result.failed > 0) { ImmichToast.show( context: context, - msg: 'assets_cannot_be_added_to_album_count'.t(context: context, args: {'count': result.failed}), + msg: context.t.assets_cannot_be_added_to_album_count(count: result.failed), toastType: ToastType.error, ); } else { ImmichToast.show( context: context, - msg: 'add_to_album_bottom_sheet_already_exists'.t(args: {"album": album.name}), + msg: context.t.add_to_album_bottom_sheet_already_exists(album: album.name), ); } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index d5407e768f..1fb78703a1 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -1,8 +1,8 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; @@ -54,14 +54,14 @@ class _GeneralBottomSheetState extends ConsumerState { } if (!result.success) { - ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + ImmichToast.show(context: context, msg: context.t.scaffold_body_error_occurred, toastType: ToastType.error); return; } ImmichToast.show( context: context, msg: result.count == 0 - ? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}) - : 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + ? context.t.add_to_album_bottom_sheet_already_exists(album: album.name) + : context.t.add_to_album_bottom_sheet_added(album: album.name), ); } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart index 94e708015c..74d5ec96da 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart @@ -1,8 +1,8 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; @@ -45,15 +45,15 @@ class _LocalAlbumBottomSheetState extends ConsumerState { } if (!result.success) { - ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error); + ImmichToast.show(context: context, msg: context.t.scaffold_body_error_occurred, toastType: ToastType.error); return; } ImmichToast.show( context: context, msg: result.count == 0 - ? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}) - : 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), + ? context.t.add_to_album_bottom_sheet_already_exists(album: album.name) + : context.t.add_to_album_bottom_sheet_added(album: album.name), ); } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart index c764669cf6..0c63cde140 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart @@ -1,8 +1,8 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/download.action.dart'; @@ -44,15 +44,15 @@ class _PartnerDetailBottomSheetState extends ConsumerState } if (!result.success) { - ImmichToast.show( - context: context, - msg: 'scaffold_body_error_occurred'.t(context: context), - toastType: ToastType.error, - ); + ImmichToast.show(context: context, msg: context.t.scaffold_body_error_occurred, toastType: ToastType.error); return; } ImmichToast.show( context: context, msg: result.count == 0 - ? 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}) - : 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}), + ? context.t.add_to_album_bottom_sheet_already_exists(album: album.name) + : context.t.add_to_album_bottom_sheet_added(album: album.name), ); } diff --git a/mobile/lib/presentation/widgets/feature_message/feature_message_dialog.widget.dart b/mobile/lib/presentation/widgets/feature_message/feature_message_dialog.widget.dart index d748452c0e..73dcd054da 100644 --- a/mobile/lib/presentation/widgets/feature_message/feature_message_dialog.widget.dart +++ b/mobile/lib/presentation/widgets/feature_message/feature_message_dialog.widget.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:math' as math; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/feature_message.model.dart'; @@ -263,12 +262,12 @@ class _FeaturePage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - highlight.titleKey.tr(), + highlight.title(context.t), style: context.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700, fontSize: 24), ), const SizedBox(height: 8), Text( - highlight.bodyKey.tr(), + highlight.body(context.t), style: context.textTheme.bodyLarge?.copyWith(color: scheme.onSurfaceVariant, height: 1.4), ), ], diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index aaf3e0dbaf..bdedd29d45 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -155,6 +155,7 @@ ImageProvider getFullImageProvider( Size size = const Size(1080, 1920), bool edited = true, String? localFilePath, + Size? remoteThumbnailSize, }) { // Create new provider and cache it final ImageProvider provider; @@ -189,13 +190,21 @@ ImageProvider getFullImageProvider( assetType: asset.type, isAnimated: asset.isAnimatedImage, edited: edited, + thumbnailSize: remoteThumbnailSize, ); } return provider; } -ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution, bool edited = true}) { +ImageProvider? getThumbnailImageProvider( + BaseAsset asset, { + Size size = kThumbnailResolution, + + /// Physical size to decode for remote thumbnails, or null for the source size. + Size? remoteSize, + bool edited = true, +}) { if (_shouldUseLocalAsset(asset)) { final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; return LocalThumbProvider(id: id, size: size, assetType: asset.type, checksum: asset.checksum); @@ -203,7 +212,9 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId; final thumbhash = asset is RemoteAsset ? asset.thumbHash ?? "" : ""; - return assetId != null ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash, edited: edited) : null; + return assetId != null + ? RemoteImageProvider.thumbnail(assetId: assetId, thumbhash: thumbhash, edited: edited, decodeSize: remoteSize) + : null; } bool _shouldUseLocalAsset(BaseAsset asset) => diff --git a/mobile/lib/presentation/widgets/images/remote_image_provider.dart b/mobile/lib/presentation/widgets/images/remote_image_provider.dart index 9618ee72ad..b533fd8cf5 100644 --- a/mobile/lib/presentation/widgets/images/remote_image_provider.dart +++ b/mobile/lib/presentation/widgets/images/remote_image_provider.dart @@ -14,10 +14,17 @@ class RemoteImageProvider extends CancellableImageProvider final String url; final bool edited; - RemoteImageProvider({required this.url, this.edited = true}); + /// Physical size to decode, or null for the source size. + final Size? decodeSize; - RemoteImageProvider.thumbnail({required String assetId, required String thumbhash, this.edited = true}) - : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash, edited: edited); + RemoteImageProvider({required this.url, this.edited = true, this.decodeSize}); + + RemoteImageProvider.thumbnail({ + required String assetId, + required String thumbhash, + this.edited = true, + this.decodeSize, + }) : url = getThumbnailUrlForRemoteId(assetId, thumbhash: thumbhash, edited: edited); @override Future obtainKey(ImageConfiguration configuration) { @@ -37,7 +44,7 @@ class RemoteImageProvider extends CancellableImageProvider } Stream _codec(RemoteImageProvider key, ImageDecoderCallback decode) { - final request = this.request = RemoteImageRequest(uri: key.url); + final request = this.request = RemoteImageRequest(uri: key.url, decodeSize: key.decodeSize); return loadRequest(request, decode, isFinal: true); } @@ -47,13 +54,13 @@ class RemoteImageProvider extends CancellableImageProvider return true; } if (other is RemoteImageProvider) { - return url == other.url && edited == other.edited; + return url == other.url && edited == other.edited && decodeSize == other.decodeSize; } return false; } @override - int get hashCode => url.hashCode ^ edited.hashCode; + int get hashCode => url.hashCode ^ edited.hashCode ^ decodeSize.hashCode; } class RemoteFullImageProvider extends CancellableImageProvider @@ -64,12 +71,16 @@ class RemoteFullImageProvider extends CancellableImageProvider [ DiagnosticsProperty('Image provider', this), DiagnosticsProperty('Asset Id', key.assetId), @@ -96,7 +109,12 @@ class RemoteFullImageProvider extends CancellableImageProvider [ DiagnosticsProperty('Image provider', this), diff --git a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart index 90bb79cced..5bfeb25977 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart @@ -26,17 +26,22 @@ class Thumbnail extends StatefulWidget { required String remoteId, required String thumbhash, this.fit = BoxFit.cover, - Size size = kThumbnailResolution, + + /// Physical size to decode, or null for the source size. + Size? decodeSize, super.key, - }) : imageProvider = RemoteImageProvider.thumbnail(assetId: remoteId, thumbhash: thumbhash), + }) : imageProvider = RemoteImageProvider.thumbnail(assetId: remoteId, thumbhash: thumbhash, decodeSize: decodeSize), thumbhashProvider = null; Thumbnail.fromAsset({ required BaseAsset? asset, this.fit = BoxFit.cover, - /// The logical UI size of the thumbnail. This is only used to determine the ideal image resolution and does not affect the widget size. + /// Decode size for local thumbnails. This does not affect the widget size. Size size = kThumbnailResolution, + + /// Physical size to decode for remote thumbnails. + Size? remoteSize, super.key, }) : thumbhashProvider = switch (asset) { RemoteAsset() when asset.thumbHash != null && asset.localId == null => ThumbHashProvider( @@ -44,7 +49,7 @@ class Thumbnail extends StatefulWidget { ), _ => null, }, - imageProvider = asset == null ? null : getThumbnailImageProvider(asset, size: size); + imageProvider = asset == null ? null : getThumbnailImageProvider(asset, size: size, remoteSize: remoteSize); @override State createState() => _ThumbnailState(); diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart index 1d3dcb2cf0..953efc144d 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -16,6 +16,7 @@ class ThumbnailTile extends ConsumerStatefulWidget { const ThumbnailTile( this.asset, { this.size = kThumbnailResolution, + this.remoteSize, this.fit = BoxFit.cover, this.showStorageIndicator = false, this.lockSelection = false, @@ -26,6 +27,9 @@ class ThumbnailTile extends ConsumerStatefulWidget { final BaseAsset? asset; final Size size; + + /// Physical size to decode for remote thumbnails. + final Size? remoteSize; final BoxFit fit; final bool showStorageIndicator; final bool lockSelection; @@ -108,7 +112,7 @@ class _ThumbnailTileState extends ConsumerState { // but other solutions have failed thus far. key: ValueKey(isCurrentAsset), tag: '${asset?.heroTag}_$heroIndex', - child: Thumbnail.fromAsset(asset: asset, size: widget.size), + child: Thumbnail.fromAsset(asset: asset, size: widget.size, remoteSize: widget.remoteSize), // Placeholderbuilder used to hide indicators on first hero animation, since flightShuttleBuilder isn't called until both source and destination hero exist in widget tree. placeholderBuilder: (context, heroSize, child) { if (!_hideIndicators) { diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index 918d64054b..ab2db23d8b 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; @@ -164,7 +164,7 @@ class _DriftMapState extends ConsumerState { context: context, gravity: ToastGravity.BOTTOM, toastType: ToastType.error, - msg: "map_cannot_get_user_location".t(context: context), + msg: context.t.map_cannot_get_user_location, ); } return; diff --git a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart index 2680850bfe..c2a5678ee9 100644 --- a/mobile/lib/presentation/widgets/map/map_settings_sheet.dart +++ b/mobile/lib/presentation/widgets/map/map_settings_sheet.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/time_range.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:immich_mobile/widgets/map/map_settings/map_custom_time_range.dart'; @@ -50,18 +49,18 @@ class _DriftMapSettingsSheetState extends ConsumerState { ), const Divider(height: 30, thickness: 1), MapSettingsListTile( - title: "map_settings_only_show_favorites".t(context: context), + title: context.t.map_settings_only_show_favorites, selected: mapState.onlyFavorites, onChanged: (favoriteOnly) => ref.read(mapStateProvider.notifier).switchFavoriteOnly(favoriteOnly), ), MapSettingsListTile( - title: "map_settings_include_show_archived".t(context: context), + title: context.t.map_settings_include_show_archived, selected: mapState.includeArchived, onChanged: (includeArchive) => ref.read(mapStateProvider.notifier).switchIncludeArchived(includeArchive), ), MapSettingsListTile( - title: "map_settings_include_show_partners".t(context: context), + title: context.t.map_settings_include_show_partners, selected: mapState.withPartners, onChanged: (withPartners) => ref.read(mapStateProvider.notifier).switchWithPartners(withPartners), ), diff --git a/mobile/lib/presentation/widgets/map/map_utils.dart b/mobile/lib/presentation/widgets/map/map_utils.dart index 34116b144b..3ed6ceee25 100644 --- a/mobile/lib/presentation/widgets/map/map_utils.dart +++ b/mobile/lib/presentation/widgets/map/map_utils.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:logging/logging.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -124,10 +124,10 @@ class MapUtils { class _LocationServiceDisabledDialog extends ConfirmDialog { _LocationServiceDisabledDialog(BuildContext context) : super( - title: 'map_location_service_disabled_title'.t(context: context), - content: 'map_location_service_disabled_content'.t(context: context), - cancel: 'cancel'.t(context: context), - ok: 'yes'.t(context: context), + title: context.t.map_location_service_disabled_title, + content: context.t.map_location_service_disabled_content, + cancel: context.t.cancel, + ok: context.t.yes, onOk: () async { await Geolocator.openLocationSettings(); }, @@ -137,10 +137,10 @@ class _LocationServiceDisabledDialog extends ConfirmDialog { class _LocationPermissionDisabledDialog extends ConfirmDialog { _LocationPermissionDisabledDialog(BuildContext context) : super( - title: 'map_no_location_permission_title'.t(context: context), - content: 'map_no_location_permission_content'.t(context: context), - cancel: 'cancel'.t(context: context), - ok: 'yes'.t(context: context), + title: context.t.map_no_location_permission_title, + content: context.t.map_no_location_permission_content, + cancel: context.t.cancel, + ok: context.t.yes, onOk: () {}, ); } diff --git a/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart b/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart index fc9bfb34e7..2bafdc0843 100644 --- a/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_bottom_info.widget.dart @@ -1,12 +1,13 @@ // ignore_for_file: require_trailing_commas import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:intl/intl.dart'; class DriftMemoryBottomInfo extends StatelessWidget { final DriftMemory memory; @@ -36,7 +37,7 @@ class DriftMemoryBottomInfo extends StatelessWidget { ], ), Tooltip( - message: 'view_in_timeline'.tr(), + message: context.t.view_in_timeline, child: MaterialButton( minWidth: 0, onPressed: () async { diff --git a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart index 94a91437b0..1732249523 100644 --- a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart @@ -4,7 +4,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/pages/drift_memory.page.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; @@ -53,7 +53,7 @@ class DriftMemoryCard extends StatelessWidget { @override Widget build(BuildContext context) { final yearsAgo = DateTime.now().year - memory.data.year; - final title = 'years_ago'.t(context: context, args: {'years': yearsAgo.toString()}); + final title = context.t.years_ago(years: yearsAgo); return Center( child: Stack( children: [ diff --git a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart index b5a9dd235c..e56a21e021 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart @@ -4,14 +4,14 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:scroll_date_picker/scroll_date_picker.dart'; class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget { - final DriftPerson person; + final Person person; const DriftPersonBirthdayEditForm({super.key, required this.person}); @@ -30,14 +30,9 @@ class _DriftPersonNameEditFormState extends ConsumerState saveBirthday() async { try { - final result = await ref.read(driftPeopleServiceProvider).updateBrithday(widget.person.id, _selectedDate); - - if (result != 0) { - ref.invalidate(driftGetAllPeopleProvider); - if (!mounted) { - return; - } + final result = await ref.read(driftPeopleServiceProvider).updateBirthday(widget.person.id, _selectedDate); + if (result != 0 && mounted) { context.pop(_selectedDate); } } catch (error) { @@ -49,7 +44,7 @@ class _DriftPersonNameEditFormState extends ConsumerState context.pop(null), child: Text( - "cancel", + context.t.cancel, style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold), - ).tr(), + ), ), TextButton( onPressed: () => saveBirthday(), child: Text( - "save", + context.t.save, style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), + ), ), ], ); diff --git a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart index bb63de3695..4ae41bc640 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart @@ -1,16 +1,15 @@ -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/domain/models/person.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class DriftPersonNameEditForm extends ConsumerStatefulWidget { - final DriftPerson person; + final Person person; const DriftPersonNameEditForm({super.key, required this.person}); @@ -30,12 +29,7 @@ class _DriftPersonNameEditFormState extends ConsumerState onEdit(String personId, String newName) async { try { final result = await ref.read(driftPeopleServiceProvider).updateName(personId, newName); - if (result != 0) { - ref.invalidate(driftGetAllPeopleProvider); - if (!mounted) { - return; - } - + if (result != 0 && mounted) { context.pop(newName); } } catch (error) { @@ -47,7 +41,7 @@ class _DriftPersonNameEditFormState extends ConsumerState context.pop(null), child: Text( - "cancel", + context.t.cancel, style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold), - ).tr(), + ), ), TextButton( onPressed: () => onEdit(widget.person.id, _formController.text), child: Text( - "save", + context.t.save, style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), + ), ), ], ); diff --git a/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart b/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart index ac2d4580c9..026a28bd7d 100644 --- a/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class PersonOptionSheet extends StatelessWidget { const PersonOptionSheet({super.key, this.onEditName, this.onEditBirthday, this.birthdayExists = false}); @@ -20,12 +20,12 @@ class PersonOptionSheet extends StatelessWidget { children: [ ListTile( leading: const Icon(Icons.edit), - title: Text('edit_name'.t(context: context), style: textStyle), + title: Text(context.t.edit_name, style: textStyle), onTap: onEditName, ), ListTile( leading: const Icon(Icons.cake), - title: Text((birthdayExists ? 'edit_birthday' : "add_birthday").t(context: context), style: textStyle), + title: Text(birthdayExists ? context.t.edit_birthday : context.t.add_birthday, style: textStyle), onTap: onEditBirthday, ), ], diff --git a/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart b/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart index cd94ef89a8..dc89a25788 100644 --- a/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart +++ b/mobile/lib/presentation/widgets/remote_album/drift_album_option.widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; class DriftRemoteAlbumOption extends StatelessWidget { @@ -36,19 +36,14 @@ class DriftRemoteAlbumOption extends StatelessWidget { if (onEditAlbum != null) { menuChildren.add( - BaseActionButton( - label: 'edit_album'.t(context: context), - iconData: Icons.edit, - onPressed: onEditAlbum, - menuItem: true, - ), + BaseActionButton(label: context.t.edit_album, iconData: Icons.edit, onPressed: onEditAlbum, menuItem: true), ); } if (onAddPhotos != null) { menuChildren.add( BaseActionButton( - label: 'add_photos'.t(context: context), + label: context.t.add_photos, iconData: Icons.add_a_photo, onPressed: onAddPhotos, menuItem: true, @@ -59,7 +54,7 @@ class DriftRemoteAlbumOption extends StatelessWidget { if (onAddUsers != null) { menuChildren.add( BaseActionButton( - label: 'album_viewer_page_share_add_users'.t(context: context), + label: context.t.album_viewer_page_share_add_users, iconData: Icons.group_add, onPressed: onAddUsers, menuItem: true, @@ -70,7 +65,7 @@ class DriftRemoteAlbumOption extends StatelessWidget { if (onLeaveAlbum != null) { menuChildren.add( BaseActionButton( - label: 'leave_album'.t(context: context), + label: context.t.leave_album, iconData: Icons.person_remove_rounded, onPressed: onLeaveAlbum, menuItem: true, @@ -81,7 +76,7 @@ class DriftRemoteAlbumOption extends StatelessWidget { if (onToggleAlbumOrder != null) { menuChildren.add( BaseActionButton( - label: 'change_display_order'.t(context: context), + label: context.t.change_display_order, iconData: Icons.swap_vert_rounded, onPressed: onToggleAlbumOrder, menuItem: true, @@ -92,7 +87,7 @@ class DriftRemoteAlbumOption extends StatelessWidget { if (onCreateSharedLink != null) { menuChildren.add( BaseActionButton( - label: 'create_shared_link'.t(context: context), + label: context.t.create_shared_link, iconData: Icons.link, onPressed: onCreateSharedLink, menuItem: true, @@ -102,12 +97,7 @@ class DriftRemoteAlbumOption extends StatelessWidget { if (onShowOptions != null) { menuChildren.add( - BaseActionButton( - label: 'options'.t(context: context), - iconData: Icons.settings, - onPressed: onShowOptions, - menuItem: true, - ), + BaseActionButton(label: context.t.options, iconData: Icons.settings, onPressed: onShowOptions, menuItem: true), ); } @@ -115,7 +105,7 @@ class DriftRemoteAlbumOption extends StatelessWidget { menuChildren.add(const Divider(height: 1)); menuChildren.add( BaseActionButton( - label: 'delete_album'.t(context: context), + label: context.t.delete_album, iconData: Icons.delete, iconColor: context.isDarkTheme ? Colors.red[400] : Colors.red[800], onPressed: onDeleteAlbum, diff --git a/mobile/lib/presentation/widgets/search/quick_date_picker.dart b/mobile/lib/presentation/widgets/search/quick_date_picker.dart index e8bf3c5a43..97ea163be8 100644 --- a/mobile/lib/presentation/widgets/search/quick_date_picker.dart +++ b/mobile/lib/presentation/widgets/search/quick_date_picker.dart @@ -1,7 +1,7 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:intl/intl.dart'; sealed class DateFilterInputModel { DateTimeRange asDateTimeRange(); @@ -13,12 +13,9 @@ sealed class DateFilterInputModel { if (date.end.difference(date.start).inHours < 24) { return DateFormat.yMMMd().format(date.start.toLocal()); } else { - return 'search_filter_date_interval'.t( - context: context, - args: { - "start": DateFormat.yMMMd().format(date.start.toLocal()), - "end": DateFormat.yMMMd().format(date.end.toLocal()), - }, + return context.t.search_filter_date_interval( + start: DateFormat.yMMMd().format(date.start.toLocal()), + end: DateFormat.yMMMd().format(date.end.toLocal()), ); } } @@ -38,7 +35,7 @@ class RecentMonthRangeFilter extends DateFilterInputModel { @override String asHumanReadable(BuildContext context) { - return 'last_months'.t(context: context, args: {"count": monthDelta.toString()}); + return context.t.last_months(count: monthDelta); } } @@ -62,7 +59,7 @@ class YearFilter extends DateFilterInputModel { @override String asHumanReadable(BuildContext context) { - return 'in_year'.tr(namedArgs: {"year": year.toString()}); + return context.t.in_year(year: year); } } @@ -121,14 +118,13 @@ class QuickDatePicker extends HookWidget { return null; } - Text _monthLabel(BuildContext context, int monthsFromNow) => - const Text('last_months').t(context: context, args: {"count": monthsFromNow.toString()}); + Text _monthLabel(BuildContext context, int monthsFromNow) => Text(context.t.last_months(count: monthsFromNow)); Widget _yearPicker(BuildContext context) { final size = MediaQuery.of(context).size; return Row( children: [ - const Text("in_year_selector").tr(), + Text(context.t.in_year_selector), const SizedBox(width: 15), Expanded( child: DropdownMenu( @@ -158,7 +154,7 @@ class QuickDatePicker extends HookWidget { child: IgnorePointer( ignoring: true, child: RadioListTile( - title: const Text('pick_custom_range').tr(), + title: Text(context.t.pick_custom_range), subtitle: hasPreviousInput ? Text(currentInput!.asHumanReadable(context)) : null, secondary: hasPreviousInput ? const Icon(Icons.edit) : null, value: _QuickPickerType.custom, diff --git a/mobile/lib/presentation/widgets/timeline/constants.dart b/mobile/lib/presentation/widgets/timeline/constants.dart index 84892c79f7..86c44d4a7f 100644 --- a/mobile/lib/presentation/widgets/timeline/constants.dart +++ b/mobile/lib/presentation/widgets/timeline/constants.dart @@ -9,5 +9,5 @@ const double kScrubberThumbHeight = 48.0; const Duration kTimelineScrubberFadeInDuration = Duration(milliseconds: 300); const Duration kTimelineScrubberFadeOutDuration = Duration(milliseconds: 800); -const Size kThumbnailResolution = Size.square(320); // TODO: make the resolution vary based on actual tile size +const Size kThumbnailResolution = Size.square(320); const kThumbnailDiskCacheSize = 1024 << 20; // 1GiB diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart index b108acde3b..32bbffa95e 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart @@ -144,19 +144,6 @@ class _FixedSegmentRow extends ConsumerWidget { TimelineService timelineService, bool isDynamicLayout, ) { - final children = [ - for (int i = 0; i < assets.length; i++) - TimelineAssetIndexWrapper( - assetIndex: assetIndex + i, - segmentIndex: 0, // For simplicity, using 0 for now - child: _AssetTileWidget( - key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)), - asset: assets[i], - assetIndex: assetIndex + i, - ), - ), - ]; - final widths = List.filled(assets.length, tileHeight); if (isDynamicLayout) { @@ -186,6 +173,20 @@ class _FixedSegmentRow extends ConsumerWidget { } } + final children = [ + for (int i = 0; i < assets.length; i++) + TimelineAssetIndexWrapper( + assetIndex: assetIndex + i, + segmentIndex: 0, // For simplicity, using 0 for now + child: _AssetTileWidget( + key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)), + asset: assets[i], + assetIndex: assetIndex + i, + size: Size(widths[i], tileHeight), + ), + ), + ]; + return TimelineDragRegion( child: TimelineRow( height: tileHeight, @@ -201,10 +202,18 @@ class _FixedSegmentRow extends ConsumerWidget { class _AssetTileWidget extends ConsumerWidget { final BaseAsset asset; final int assetIndex; + final Size size; - const _AssetTileWidget({super.key, required this.asset, required this.assetIndex}); + const _AssetTileWidget({super.key, required this.asset, required this.assetIndex, required this.size}); - Future _handleOnTap(BuildContext ctx, WidgetRef ref, int assetIndex, BaseAsset asset, int? heroOffset) async { + Future _handleOnTap( + BuildContext ctx, + WidgetRef ref, + int assetIndex, + BaseAsset asset, + int? heroOffset, + Size remoteSize, + ) async { final multiSelectState = ref.read(multiSelectProvider); if (multiSelectState.forceEnable || multiSelectState.isEnabled) { @@ -216,7 +225,7 @@ class _AssetTileWidget extends ConsumerWidget { } ref.read(isPlayingMotionVideoProvider.notifier).playing = false; - AssetViewer.setAsset(ref, asset); + AssetViewer.setAsset(ref, asset, thumbnailSize: remoteSize); unawaited( ctx.pushRoute( AssetViewerRoute( @@ -256,6 +265,8 @@ class _AssetTileWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final remoteSize = size * MediaQuery.devicePixelRatioOf(context); + final heroOffset = TabsRouterScope.of(context)?.controller.activeIndex ?? 0; final lockSelection = _getLockSelectionStatus(ref); @@ -265,10 +276,11 @@ class _AssetTileWidget extends ConsumerWidget { return RepaintBoundary( child: GestureDetector( - onTap: () => lockSelection ? null : _handleOnTap(context, ref, assetIndex, asset, heroOffset), + onTap: () => lockSelection ? null : _handleOnTap(context, ref, assetIndex, asset, heroOffset, remoteSize), onLongPress: () => lockSelection || isReadonlyModeEnabled ? null : _handleOnLongPress(ref, asset), child: ThumbnailTile( asset, + remoteSize: remoteSize, lockSelection: lockSelection, showStorageIndicator: showStorageIndicator, showStackIndicator: showStackIndicator, diff --git a/mobile/lib/presentation/widgets/timeline/timeline.state.dart b/mobile/lib/presentation/widgets/timeline/timeline.state.dart index bcdc3b4783..adad06b987 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.state.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.state.dart @@ -12,45 +12,17 @@ import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; part 'timeline.state.freezed.dart'; -class TimelineArgs { - final double maxWidth; - final double maxHeight; - final double spacing; - final int columnCount; - final bool showStorageIndicator; - final bool withStack; - final GroupAssetsBy? groupBy; - - const TimelineArgs({ - required this.maxWidth, - required this.maxHeight, - this.spacing = kTimelineSpacing, - this.columnCount = kTimelineColumnCount, - this.showStorageIndicator = false, - this.withStack = false, - this.groupBy, - }); - - @override - bool operator ==(covariant TimelineArgs other) { - return spacing == other.spacing && - maxWidth == other.maxWidth && - maxHeight == other.maxHeight && - columnCount == other.columnCount && - showStorageIndicator == other.showStorageIndicator && - withStack == other.withStack && - groupBy == other.groupBy; - } - - @override - int get hashCode => - maxWidth.hashCode ^ - maxHeight.hashCode ^ - spacing.hashCode ^ - columnCount.hashCode ^ - showStorageIndicator.hashCode ^ - withStack.hashCode ^ - groupBy.hashCode; +@freezed +abstract class TimelineArgs with _$TimelineArgs { + const factory TimelineArgs({ + required double maxWidth, + required double maxHeight, + @Default(kTimelineSpacing) double spacing, + @Default(kTimelineColumnCount) int columnCount, + @Default(false) bool showStorageIndicator, + @Default(false) bool withStack, + GroupAssetsBy? groupBy, + }) = _TimelineArgs; } @freezed diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 9e65dcb72c..c41f88fcc0 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -26,6 +26,7 @@ import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.da import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart'; import 'package:immich_mobile/widgets/common/selection_sliver_app_bar.dart'; @@ -190,7 +191,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi // may be in a background tab. In either case, `handleStatusBarTap()` still fires // Make sure the timeline is the primary route before scrolling to the top final routeData = context.findAncestorWidgetOfExactType()?.routeData; - if (ModalRoute.of(context)?.isCurrent == true && routeData?.isActive == true) { + // The tap is generated async, so it can arrive after a route pop has started (due to a back button or similar) + // Check if route is alive and not exiting before taking action + final observers = Navigator.maybeOf(context)?.widget.observers ?? const []; + final isRouteTransitioning = observers.whereType().any( + (observer) => observer.hasTransitioningRoute, + ); + + if (ModalRoute.of(context)?.isCurrent == true && routeData?.isActive == true && !isRouteTransitioning) { _scrollToTop(); } } diff --git a/mobile/lib/presentation/widgets/timeline/timeline_drag_region.dart b/mobile/lib/presentation/widgets/timeline/timeline_drag_region.dart index 9ffcc3b23b..892d0c6102 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline_drag_region.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline_drag_region.dart @@ -4,6 +4,9 @@ import 'package:collection/collection.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'timeline_drag_region.freezed.dart'; class TimelineDragRegion extends StatefulWidget { final Widget child; @@ -206,21 +209,7 @@ class _TimelineAssetIndexProxy extends RenderProxyBox { _TimelineAssetIndexProxy({required this.index}); } -class TimelineAssetIndex { - final int assetIndex; - final int segmentIndex; - - const TimelineAssetIndex({required this.assetIndex, required this.segmentIndex}); - - @override - bool operator ==(covariant TimelineAssetIndex other) { - if (identical(this, other)) { - return true; - } - - return other.assetIndex == assetIndex && other.segmentIndex == segmentIndex; - } - - @override - int get hashCode => assetIndex.hashCode ^ segmentIndex.hashCode; +@freezed +abstract class TimelineAssetIndex with _$TimelineAssetIndex { + const factory TimelineAssetIndex({required int assetIndex, required int segmentIndex}) = _TimelineAssetIndex; } diff --git a/mobile/lib/providers/album/album_sort_by_options.provider.dart b/mobile/lib/providers/album/album_sort_by_options.provider.dart index ec4ae71d03..015a846de8 100644 --- a/mobile/lib/providers/album/album_sort_by_options.provider.dart +++ b/mobile/lib/providers/album/album_sort_by_options.provider.dart @@ -1,19 +1,28 @@ import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; // Store index allows us to re-arrange the values without affecting the saved prefs enum AlbumSortMode { - title(1, "library_page_sort_title", SortOrder.asc), - assetCount(4, "library_page_sort_asset_count", SortOrder.desc), - lastModified(3, "library_page_sort_last_modified", SortOrder.desc), - created(0, "library_page_sort_created", SortOrder.desc), - mostRecent(2, "sort_recent", SortOrder.desc), - mostOldest(5, "sort_oldest", SortOrder.asc); + title(1, SortOrder.asc), + assetCount(4, SortOrder.desc), + lastModified(3, SortOrder.desc), + created(0, SortOrder.desc), + mostRecent(2, SortOrder.desc), + mostOldest(5, SortOrder.asc); final int storeIndex; - final String label; final SortOrder defaultOrder; - const AlbumSortMode(this.storeIndex, this.label, this.defaultOrder); + const AlbumSortMode(this.storeIndex, this.defaultOrder); + + String label(Translations t) => switch (this) { + AlbumSortMode.title => t.library_page_sort_title, + AlbumSortMode.assetCount => t.library_page_sort_asset_count, + AlbumSortMode.lastModified => t.library_page_sort_last_modified, + AlbumSortMode.created => t.library_page_sort_created, + AlbumSortMode.mostRecent => t.sort_recent, + AlbumSortMode.mostOldest => t.sort_oldest, + }; SortOrder effectiveOrder(bool isReverse) => isReverse ? defaultOrder.reverse() : defaultOrder; } diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 7dc475ab93..f369a37142 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -112,6 +112,12 @@ class AppLifeCycleNotifier extends StateNotifier { await Future.delayed(const Duration(milliseconds: 500)); final backgroundManager = _ref.read(backgroundSyncProvider); + + // Drop any sync that froze mid-flight while the app was suspended so resume + // starts fresh instead of awaiting the stale task (#28082). cancelResumeSyncs + // clears the task refs synchronously, so the syncs below see a clean slate. + unawaited(backgroundManager.cancelResumeSyncs()); + final isAlbumLinkedSyncEnable = _ref.read(appConfigProvider).backup.syncAlbums; try { diff --git a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart index ac929d2b6a..fd7fbebe36 100644 --- a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -18,6 +19,9 @@ abstract class AssetViewerState with _$AssetViewerState { @Default(false) bool isZoomed, @Default(false) bool showingOcr, BaseAsset? currentAsset, + + /// Physical thumbnail size retained while paging through the viewer. + Size? thumbnailSize, @Default(0) int stackIndex, }) = _AssetViewerState; } @@ -37,11 +41,17 @@ class AssetViewerStateNotifier extends Notifier { state = const AssetViewerState(); } - void setAsset(BaseAsset asset) { + void setAsset(BaseAsset asset, {Size? thumbnailSize}) { if (asset == state.currentAsset) { return; } - state = state.copyWith(currentAsset: asset, stackIndex: 0, showingOcr: false); + // Swiping to a neighbor passes no size; keep the tapped tile's so neighbors reuse it. + state = state.copyWith( + currentAsset: asset, + thumbnailSize: thumbnailSize ?? state.thumbnailSize, + stackIndex: 0, + showingOcr: false, + ); _watchCurrentAsset(asset); } diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart index 1756d0fd30..c3e1098e4b 100644 --- a/mobile/lib/providers/backup/drift_backup.provider.dart +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -44,87 +44,22 @@ abstract class DriftUploadStatus with _$DriftUploadStatus { enum BackupError { none, syncFailed } -class DriftBackupState { - final int totalCount; - final int backupCount; - final int remainderCount; - final int processingCount; +@freezed +abstract class DriftBackupState with _$DriftBackupState { + const DriftBackupState._(); - final bool isSyncing; - final BackupError error; - - final Map uploadItems; - - final Map iCloudDownloadProgress; - - const DriftBackupState({ - required this.totalCount, - required this.backupCount, - required this.remainderCount, - required this.processingCount, - required this.isSyncing, - this.error = BackupError.none, - required this.uploadItems, - this.iCloudDownloadProgress = const {}, - }); - - DriftBackupState copyWith({ - int? totalCount, - int? backupCount, - int? remainderCount, - int? processingCount, - bool? isSyncing, - BackupError? error, - Map? uploadItems, - Map? iCloudDownloadProgress, - }) { - return DriftBackupState( - totalCount: totalCount ?? this.totalCount, - backupCount: backupCount ?? this.backupCount, - remainderCount: remainderCount ?? this.remainderCount, - processingCount: processingCount ?? this.processingCount, - isSyncing: isSyncing ?? this.isSyncing, - error: error ?? this.error, - uploadItems: uploadItems ?? this.uploadItems, - iCloudDownloadProgress: iCloudDownloadProgress ?? this.iCloudDownloadProgress, - ); - } + const factory DriftBackupState({ + required int totalCount, + required int backupCount, + required int remainderCount, + required int processingCount, + required bool isSyncing, + @Default(BackupError.none) BackupError error, + required Map uploadItems, + @Default({}) Map iCloudDownloadProgress, + }) = _DriftBackupState; int get errorCount => uploadItems.values.where((item) => item.isFailed == true).length; - - @override - String toString() { - return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, isSyncing: $isSyncing, error: $error, uploadItems: $uploadItems, iCloudDownloadProgress: $iCloudDownloadProgress)'; - } - - @override - bool operator ==(covariant DriftBackupState other) { - if (identical(this, other)) { - return true; - } - final mapEquals = const DeepCollectionEquality().equals; - - return other.totalCount == totalCount && - other.backupCount == backupCount && - other.remainderCount == remainderCount && - other.processingCount == processingCount && - other.isSyncing == isSyncing && - other.error == error && - mapEquals(other.iCloudDownloadProgress, iCloudDownloadProgress) && - mapEquals(other.uploadItems, uploadItems); - } - - @override - int get hashCode { - return totalCount.hashCode ^ - backupCount.hashCode ^ - remainderCount.hashCode ^ - processingCount.hashCode ^ - isSyncing.hashCode ^ - error.hashCode ^ - uploadItems.hashCode ^ - iCloudDownloadProgress.hashCode; - } } final driftBackupProvider = StateNotifierProvider((ref) { diff --git a/mobile/lib/providers/infrastructure/people.provider.dart b/mobile/lib/providers/infrastructure/people.provider.dart index 1939940975..ae24f044e8 100644 --- a/mobile/lib/providers/infrastructure/people.provider.dart +++ b/mobile/lib/providers/infrastructure/people.provider.dart @@ -14,13 +14,13 @@ final driftPeopleServiceProvider = Provider( (ref) => DriftPeopleService(ref.watch(driftPeopleRepositoryProvider), ref.watch(personApiRepositoryProvider)), ); -final driftPeopleAssetProvider = FutureProvider.family, String>((ref, assetId) async { +final driftPeopleAssetProvider = FutureProvider.family, String>((ref, assetId) async { final service = ref.watch(driftPeopleServiceProvider); return service.getAssetPeople(assetId); }); -final driftGetAllPeopleProvider = FutureProvider>((ref) async { +final getAllPeopleProvider = StreamProvider>((ref) async* { final service = ref.watch(driftPeopleServiceProvider); final prefs = await ref.watch(userMetadataPreferencesProvider.future); - return service.getAllPeople(minFaces: prefs?.minimumFaces ?? 3); + yield* service.watch(minFaces: prefs?.minimumFaces ?? 3); }); diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index 4457b9f831..b76a8213c6 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -2,7 +2,7 @@ import 'dart:async'; -import 'package:collection/collection.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -17,30 +17,17 @@ import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:logging/logging.dart'; -class RemoteAlbumState { - final List albums; +part 'remote_album.provider.freezed.dart'; - const RemoteAlbumState({required this.albums}); +@Freezed(toStringOverride: false) +abstract class RemoteAlbumState with _$RemoteAlbumState { + const RemoteAlbumState._(); - RemoteAlbumState copyWith({List? albums}) { - return RemoteAlbumState(albums: albums ?? this.albums); - } + const factory RemoteAlbumState({required List albums}) = _RemoteAlbumState; + // Explicitly don't log albums @override String toString() => 'RemoteAlbumState(albums: ${albums.length})'; - - @override - bool operator ==(covariant RemoteAlbumState other) { - if (identical(this, other)) { - return true; - } - final listEquals = const DeepCollectionEquality().equals; - - return listEquals(other.albums, albums); - } - - @override - int get hashCode => albums.hashCode; } class RemoteAlbumNotifier extends Notifier { diff --git a/mobile/lib/providers/local_auth.provider.dart b/mobile/lib/providers/local_auth.provider.dart index 58bd4fb0f8..d99f28823f 100644 --- a/mobile/lib/providers/local_auth.provider.dart +++ b/mobile/lib/providers/local_auth.provider.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/auth/biometric_status.model.dart'; import 'package:immich_mobile/services/local_auth.service.dart'; import 'package:immich_mobile/services/secure_storage.service.dart'; @@ -43,6 +43,7 @@ class LocalAuthNotifier extends StateNotifier { } Future authenticate(BuildContext context, String? message) async { + final t = StaticTranslations.instance; String errorMessage = ""; try { @@ -51,20 +52,20 @@ class LocalAuthNotifier extends StateNotifier { switch (error.code) { case "NotEnrolled": _log.warning("User is not enrolled in biometrics"); - errorMessage = "biometric_no_options".tr(); + errorMessage = t.biometric_no_options; case "NotAvailable": _log.warning("Biometric authentication is not available"); - errorMessage = "biometric_not_available".tr(); + errorMessage = t.biometric_not_available; case "LockedOut": _log.warning("User is locked out of biometric authentication"); - errorMessage = "biometric_locked_out".tr(); + errorMessage = t.biometric_locked_out; default: _log.warning("Failed to authenticate with unknown reason"); - errorMessage = 'failed_to_authenticate'.tr(); + errorMessage = t.failed_to_authenticate; } } catch (error) { _log.warning("Error during authentication: $error"); - errorMessage = 'failed_to_authenticate'.tr(); + errorMessage = t.failed_to_authenticate; } finally { if (errorMessage.isNotEmpty) { context.showSnackBar( diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart deleted file mode 100644 index 1bd58509f5..0000000000 --- a/mobile/lib/providers/search/people.provider.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/services/person.service.dart'; - -final getAllPeopleProvider = FutureProvider.autoDispose>((ref) async { - final PersonService personService = ref.read(personServiceProvider); - - final people = await personService.getAllPeople(); - - return people; -}); - -final updatePersonNameProvider = FutureProvider.autoDispose( - (ref) => (String personId, String updatedName) async { - final PersonService personService = ref.read(personServiceProvider); - final person = await personService.updateName(personId, updatedName); - - if (person != null && person.name == updatedName) { - ref.invalidate(getAllPeopleProvider); - return true; - } - return false; - }, -); diff --git a/mobile/lib/providers/search/search_filter.provider.dart b/mobile/lib/providers/search/search_filter.provider.dart index b171de50c7..5da0a11fe7 100644 --- a/mobile/lib/providers/search/search_filter.provider.dart +++ b/mobile/lib/providers/search/search_filter.provider.dart @@ -1,34 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/services/search.service.dart'; import 'package:openapi/api.dart'; -class SearchSuggestionArgs { - SearchSuggestionType type; - final String? locationCountry; - final String? locationState; - final String? make; - final String? model; +part 'search_filter.provider.freezed.dart'; - SearchSuggestionArgs({required this.type, this.locationCountry, this.locationState, this.make, this.model}); - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is SearchSuggestionArgs && - other.type == type && - other.locationCountry == locationCountry && - other.locationState == locationState && - other.make == make && - other.model == model; - } - - @override - int get hashCode { - return type.hashCode ^ locationCountry.hashCode ^ locationState.hashCode ^ make.hashCode ^ model.hashCode; - } +@freezed +abstract class SearchSuggestionArgs with _$SearchSuggestionArgs { + const factory SearchSuggestionArgs({ + required SearchSuggestionType type, + String? locationCountry, + String? locationState, + String? make, + String? model, + }) = _SearchSuggestionArgs; } final getSearchSuggestionsProvider = FutureProvider.autoDispose.family, SearchSuggestionArgs>(( diff --git a/mobile/lib/providers/sync_status.provider.dart b/mobile/lib/providers/sync_status.provider.dart index 7a70514ca7..eb183fbb13 100644 --- a/mobile/lib/providers/sync_status.provider.dart +++ b/mobile/lib/providers/sync_status.provider.dart @@ -1,7 +1,7 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; part 'sync_status.provider.freezed.dart'; @@ -12,11 +12,12 @@ enum SyncStatus { error; String localized() { + final t = StaticTranslations.instance; return switch (this) { - SyncStatus.idle => "idle".tr(), - SyncStatus.syncing => "running".tr(), - SyncStatus.success => "success".tr(), - SyncStatus.error => "error".tr(), + SyncStatus.idle => t.idle, + SyncStatus.syncing => t.running, + SyncStatus.success => t.success, + SyncStatus.error => t.error, }; } } diff --git a/mobile/lib/providers/timeline/multiselect.provider.dart b/mobile/lib/providers/timeline/multiselect.provider.dart index 36de8d5e4f..f74201e983 100644 --- a/mobile/lib/providers/timeline/multiselect.provider.dart +++ b/mobile/lib/providers/timeline/multiselect.provider.dart @@ -1,22 +1,27 @@ // ignore_for_file: use-ref-and-state-synchronously -import 'package:collection/collection.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +part 'multiselect.provider.freezed.dart'; + final multiSelectProvider = NotifierProvider( MultiSelectNotifier.new, dependencies: [timelineServiceProvider], ); -class MultiSelectState { - final Set selectedAssets; - final Set lockedSelectionAssets; - final bool forceEnable; +@freezed +abstract class MultiSelectState with _$MultiSelectState { + const MultiSelectState._(); - const MultiSelectState({required this.selectedAssets, required this.lockedSelectionAssets, this.forceEnable = false}); + const factory MultiSelectState({ + required Set selectedAssets, + required Set lockedSelectionAssets, + @Default(false) bool forceEnable, + }) = _MultiSelectState; bool get isEnabled => selectedAssets.isNotEmpty; @@ -29,37 +34,6 @@ class MultiSelectState { bool get onlyLocal => selectedAssets.any((asset) => asset.storage == AssetState.local); bool get onlyRemote => selectedAssets.any((asset) => asset.storage == AssetState.remote); - - MultiSelectState copyWith({ - Set? selectedAssets, - Set? lockedSelectionAssets, - bool? forceEnable, - }) { - return MultiSelectState( - selectedAssets: selectedAssets ?? this.selectedAssets, - lockedSelectionAssets: lockedSelectionAssets ?? this.lockedSelectionAssets, - forceEnable: forceEnable ?? this.forceEnable, - ); - } - - @override - String toString() => - 'MultiSelectState(selectedAssets: $selectedAssets, lockedSelectionAssets: $lockedSelectionAssets, forceEnable: $forceEnable)'; - - @override - bool operator ==(covariant MultiSelectState other) { - if (identical(this, other)) { - return true; - } - final setEquals = const DeepCollectionEquality().equals; - - return setEquals(other.selectedAssets, selectedAssets) && - setEquals(other.lockedSelectionAssets, lockedSelectionAssets) && - other.forceEnable == forceEnable; - } - - @override - int get hashCode => selectedAssets.hashCode ^ lockedSelectionAssets.hashCode ^ forceEnable.hashCode; } class MultiSelectNotifier extends Notifier { diff --git a/mobile/lib/providers/upload_profile_image.provider.dart b/mobile/lib/providers/upload_profile_image.provider.dart index 5e7f7fc732..9b5e83622e 100644 --- a/mobile/lib/providers/upload_profile_image.provider.dart +++ b/mobile/lib/providers/upload_profile_image.provider.dart @@ -1,60 +1,18 @@ -import 'dart:convert'; - +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/utils/debug_print.dart'; +part 'upload_profile_image.provider.freezed.dart'; + enum UploadProfileStatus { idle, loading, success, failure } -class UploadProfileImageState { - // enum - final UploadProfileStatus status; - final String profileImagePath; - const UploadProfileImageState({required this.status, required this.profileImagePath}); - - UploadProfileImageState copyWith({UploadProfileStatus? status, String? profileImagePath}) { - return UploadProfileImageState( - status: status ?? this.status, - profileImagePath: profileImagePath ?? this.profileImagePath, - ); - } - - Map toMap() { - final result = {}; - - result.addAll({'status': status.index}); - result.addAll({'profileImagePath': profileImagePath}); - - return result; - } - - factory UploadProfileImageState.fromMap(Map map) { - return UploadProfileImageState( - status: UploadProfileStatus.values[map['status'] ?? 0], - profileImagePath: map['profileImagePath'] ?? '', - ); - } - - String toJson() => json.encode(toMap()); - - factory UploadProfileImageState.fromJson(String source) => UploadProfileImageState.fromMap(json.decode(source)); - - @override - String toString() => 'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is UploadProfileImageState && other.status == status && other.profileImagePath == profileImagePath; - } - - @override - int get hashCode => status.hashCode ^ profileImagePath.hashCode; +@freezed +abstract class UploadProfileImageState with _$UploadProfileImageState { + const factory UploadProfileImageState({required UploadProfileStatus status, required String profileImagePath}) = + _UploadProfileImageState; } class UploadProfileImageNotifier extends StateNotifier { diff --git a/mobile/lib/repositories/biometric.repository.dart b/mobile/lib/repositories/biometric.repository.dart index 8bd5a85542..7da4bfe757 100644 --- a/mobile/lib/repositories/biometric.repository.dart +++ b/mobile/lib/repositories/biometric.repository.dart @@ -1,5 +1,5 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/auth/biometric_status.model.dart'; import 'package:local_auth/local_auth.dart'; @@ -19,6 +19,6 @@ class BiometricRepository { } Future authenticate(String? message) async { - return _localAuth.authenticate(localizedReason: message ?? 'please_auth_to_access'.tr()); + return _localAuth.authenticate(localizedReason: message ?? StaticTranslations.instance.please_auth_to_access); } } diff --git a/mobile/lib/repositories/person_api.repository.dart b/mobile/lib/repositories/person_api.repository.dart index 158e3ac333..262868eea1 100644 --- a/mobile/lib/repositories/person_api.repository.dart +++ b/mobile/lib/repositories/person_api.repository.dart @@ -11,12 +11,7 @@ class PersonApiRepository extends ApiRepository { PersonApiRepository(this._api); - Future> getAll() async { - final dto = await checkNull(_api.getAllPeople()); - return dto.people.map(_toPerson).toList(); - } - - Future update(String id, {String? name, DateTime? birthday}) async { + Future update(String id, {String? name, DateTime? birthday}) async { final birthdayUtc = birthday == null ? null : DateTime.utc(birthday.year, birthday.month, birthday.day); final dto = PersonUpdateDto( name: name == null ? const Optional.absent() : Optional.present(name), @@ -26,12 +21,6 @@ class PersonApiRepository extends ApiRepository { return _toPerson(response); } - static PersonDto _toPerson(PersonResponseDto dto) => PersonDto( - birthDate: dto.birthDate, - id: dto.id, - isHidden: dto.isHidden, - name: dto.name, - thumbnailPath: dto.thumbnailPath, - updatedAt: dto.updatedAt.orElse(null), - ); + static Person _toPerson(PersonResponseDto dto) => + .new(birthDate: dto.birthDate, id: dto.id, name: dto.name, updatedAt: dto.updatedAt.orElse(null)); } diff --git a/mobile/lib/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart index a7f6d532e9..90381cf49c 100644 --- a/mobile/lib/routing/app_navigation_observer.dart +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -30,3 +30,22 @@ class AppNavigationObserver extends AutoRouterObserver { ref.invalidate(isAssetViewerOpenProvider); } } + +/// Tracks routes that are undergoing a pop transition +class TransitioningRouteObserver extends NavigatorObserver { + int _transitioningRoutes = 0; + + /// Whether a "popping" route is still on screen + bool get hasTransitioningRoute => _transitioningRoutes > 0; + + @override + void didPop(Route route, Route? previousRoute) { + if (route is! TransitionRoute) { + return; + } + + _transitioningRoutes += 1; + // Transition completed and route disposed + unawaited(route.completed.whenComplete(() => _transitioningRoutes -= 1)); + } +} diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index 5312107f6c..b2a134389f 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart'; @@ -25,6 +26,8 @@ import 'package:logging/logging.dart'; import 'package:openapi/api.dart' as api; import 'package:path/path.dart' as p; +part 'background_upload.service.freezed.dart'; + final backgroundUploadServiceProvider = Provider((ref) { final service = BackgroundUploadService( ref.watch(uploadRepositoryProvider), @@ -39,20 +42,15 @@ final backgroundUploadServiceProvider = Provider((ref) { }); /// Metadata for upload tasks to track live photo handling -class UploadTaskMetadata { - final String localAssetId; - final bool isLivePhotos; - final String livePhotoVideoId; +@Freezed(fromJson: false, toJson: false) +abstract class UploadTaskMetadata with _$UploadTaskMetadata { + const UploadTaskMetadata._(); - const UploadTaskMetadata({required this.localAssetId, required this.isLivePhotos, required this.livePhotoVideoId}); - - UploadTaskMetadata copyWith({String? localAssetId, bool? isLivePhotos, String? livePhotoVideoId}) { - return UploadTaskMetadata( - localAssetId: localAssetId ?? this.localAssetId, - isLivePhotos: isLivePhotos ?? this.isLivePhotos, - livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId, - ); - } + const factory UploadTaskMetadata({ + required String localAssetId, + required bool isLivePhotos, + required String livePhotoVideoId, + }) = _UploadTaskMetadata; Map toMap() { return { @@ -70,28 +68,10 @@ class UploadTaskMetadata { ); } - String toJson() => json.encode(toMap()); - factory UploadTaskMetadata.fromJson(String source) => UploadTaskMetadata.fromMap(json.decode(source) as Map); - @override - String toString() => - 'UploadTaskMetadata(localAssetId: $localAssetId, isLivePhotos: $isLivePhotos, livePhotoVideoId: $livePhotoVideoId)'; - - @override - bool operator ==(covariant UploadTaskMetadata other) { - if (identical(this, other)) { - return true; - } - - return other.localAssetId == localAssetId && - other.isLivePhotos == isLivePhotos && - other.livePhotoVideoId == livePhotoVideoId; - } - - @override - int get hashCode => localAssetId.hashCode ^ isLivePhotos.hashCode ^ livePhotoVideoId.hashCode; + String toJson() => json.encode(toMap()); } /// Service for handling background uploads using iOS URLSession (background_downloader) diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index 11cbd03017..88576bbb4d 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/network_capability_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; @@ -241,16 +241,17 @@ class ForegroundUploadService { Completer? cancelToken, { required UploadCallbacks callbacks, }) async { + final t = StaticTranslations.instance; + final assetNotFoundOnDevice = CurrentPlatform.isAndroid + ? t.asset_not_found_on_device_android + : t.asset_not_found_on_device_ios; File? file; File? livePhotoFile; try { final entity = await _storageRepository.getAssetEntityForAsset(asset); if (entity == null) { - callbacks.onError?.call( - asset.localId!, - CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(), - ); + callbacks.onError?.call(asset.localId!, assetNotFoundOnDevice); return; } @@ -284,10 +285,7 @@ class ForegroundUploadService { file = await _storageRepository.getFileForAsset(asset.id); if (file == null) { _logger.warning("Failed to get file ${asset.id} - ${asset.name}"); - callbacks.onError?.call( - asset.localId!, - CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(), - ); + callbacks.onError?.call(asset.localId!, assetNotFoundOnDevice); return; } @@ -296,17 +294,14 @@ class ForegroundUploadService { livePhotoFile = await _storageRepository.getMotionFileForAsset(asset); if (livePhotoFile == null) { _logger.warning("Failed to obtain motion part of the livePhoto - ${asset.name}"); - callbacks.onError?.call( - asset.localId!, - CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(), - ); + callbacks.onError?.call(asset.localId!, assetNotFoundOnDevice); } } } if (file == null) { _logger.warning("Failed to obtain file from iCloud for asset ${asset.id} - ${asset.name}"); - callbacks.onError?.call(asset.localId!, "asset_not_found_on_icloud".t()); + callbacks.onError?.call(asset.localId!, t.asset_not_found_on_icloud); return; } diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart deleted file mode 100644 index 0d589ea71d..0000000000 --- a/mobile/lib/services/person.service.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/repositories/person_api.repository.dart'; -import 'package:logging/logging.dart'; - -final personServiceProvider = Provider.autoDispose( - (ref) => PersonService(ref.watch(personApiRepositoryProvider)), -); - -class PersonService { - final Logger _log = Logger("PersonService"); - final PersonApiRepository _personApiRepository; - PersonService(this._personApiRepository); - - Future> getAllPeople() async { - try { - return await _personApiRepository.getAll(); - } catch (error, stack) { - _log.severe("Error while fetching curated people", error, stack); - return []; - } - } - - Future updateName(String id, String name) async { - try { - return await _personApiRepository.update(id, name: name); - } catch (error, stack) { - _log.severe("Error while updating person name", error, stack); - } - return null; - } -} diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 28a9442dc7..4e0a5efff5 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -1,5 +1,4 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; @@ -7,6 +6,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; @@ -203,13 +203,13 @@ enum ActionButtonType { ), ActionButtonType.setProfilePicture => ActionMenuItem(action: SetProfilePictureAction(asset: context.asset)), ActionButtonType.openInfo => BaseActionButton( - label: 'info'.tr(), + label: StaticTranslations.instance.info, iconData: Icons.info_outline, menuItem: true, onPressed: () => EventStream.shared.emit(const ViewerShowDetailsEvent()), ), ActionButtonType.viewInTimeline => BaseActionButton( - label: 'view_in_timeline'.tr(), + label: StaticTranslations.instance.view_in_timeline, iconData: Icons.image_search, iconOnly: iconOnly, menuItem: menuItem, diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index 37ad748a57..efe6403559 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -2,7 +2,7 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; @@ -15,31 +15,33 @@ import 'package:photo_manager/photo_manager.dart'; import 'package:sqlite3/common.dart'; void configureFileDownloaderNotifications() { + final t = StaticTranslations.instance; + FileDownloader().configureNotificationForGroup( kDownloadGroupImage, - running: TaskNotification('downloading_media'.t(), '${'file_name_text'.t()}: {filename}'), - complete: TaskNotification('download_finished'.t(), '${'file_name_text'.t()}: {filename}'), + running: TaskNotification(t.downloading_media, '${t.file_name_text}: {filename}'), + complete: TaskNotification(t.download_finished, '${t.file_name_text}: {filename}'), progressBar: true, ); FileDownloader().configureNotificationForGroup( kDownloadGroupVideo, - running: TaskNotification('downloading_media'.t(), '${'file_name_text'.t()}: {filename}'), - complete: TaskNotification('download_finished'.t(), '${'file_name_text'.t()}: {filename}'), + running: TaskNotification(t.downloading_media, '${t.file_name_text}: {filename}'), + complete: TaskNotification(t.download_finished, '${t.file_name_text}: {filename}'), progressBar: true, ); FileDownloader().configureNotificationForGroup( kManualUploadGroup, - running: TaskNotification('uploading_media'.t(), 'backup_background_service_in_progress_notification'.t()), - complete: TaskNotification('upload_finished'.t(), 'backup_background_service_complete_notification'.t()), + running: TaskNotification(t.uploading_media, t.backup_background_service_in_progress_notification), + complete: TaskNotification(t.upload_finished, t.backup_background_service_complete_notification), groupNotificationId: kManualUploadGroup, ); FileDownloader().configureNotificationForGroup( kBackupGroup, - running: TaskNotification('uploading_media'.t(), 'backup_background_service_in_progress_notification'.t()), - complete: TaskNotification('upload_finished'.t(), 'backup_background_service_complete_notification'.t()), + running: TaskNotification(t.uploading_media, t.backup_background_service_in_progress_notification), + complete: TaskNotification(t.upload_finished, t.backup_background_service_complete_notification), groupNotificationId: kBackupGroup, ); } diff --git a/mobile/lib/utils/map_utils.dart b/mobile/lib/utils/map_utils.dart index d5e6f957a5..2a89b63422 100644 --- a/mobile/lib/utils/map_utils.dart +++ b/mobile/lib/utils/map_utils.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; import 'package:logging/logging.dart'; @@ -121,10 +121,10 @@ class MapUtils { class _LocationServiceDisabledDialog extends ConfirmDialog { _LocationServiceDisabledDialog() : super( - title: 'map_location_service_disabled_title'.tr(), - content: 'map_location_service_disabled_content'.tr(), - cancel: 'cancel'.tr(), - ok: 'yes'.tr(), + title: StaticTranslations.instance.map_location_service_disabled_title, + content: StaticTranslations.instance.map_location_service_disabled_content, + cancel: StaticTranslations.instance.cancel, + ok: StaticTranslations.instance.yes, onOk: () async { await Geolocator.openLocationSettings(); }, @@ -134,10 +134,10 @@ class _LocationServiceDisabledDialog extends ConfirmDialog { class _LocationPermissionDisabledDialog extends ConfirmDialog { _LocationPermissionDisabledDialog() : super( - title: 'map_no_location_permission_title'.tr(), - content: 'map_no_location_permission_content'.tr(), - cancel: 'cancel'.tr(), - ok: 'yes'.tr(), + title: StaticTranslations.instance.map_no_location_permission_title, + content: StaticTranslations.instance.map_no_location_permission_content, + cancel: StaticTranslations.instance.cancel, + ok: StaticTranslations.instance.yes, onOk: () {}, ); } diff --git a/mobile/lib/utils/people.utils.dart b/mobile/lib/utils/people.utils.dart index 18ae8a4528..bd2b607e0b 100644 --- a/mobile/lib/utils/people.utils.dart +++ b/mobile/lib/utils/people.utils.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/people/person_edit_birthday_modal.widget.dart'; import 'package:immich_mobile/presentation/widgets/people/person_edit_name_modal.widget.dart'; @@ -8,12 +8,13 @@ String formatAge(DateTime birthDate, DateTime referenceDate) { final int ageInYears = _calculateAge(birthDate, referenceDate); final int ageInMonths = _calculateAgeInMonths(birthDate, referenceDate); + final t = StaticTranslations.instance; if (ageInMonths <= 11) { - return "person_age_months".t(args: {'months': ageInMonths.toString()}); + return t.person_age_months(months: ageInMonths); } else if (ageInMonths > 12 && ageInMonths <= 23) { - return "person_age_year_months".t(args: {'months': (ageInMonths - 12).toString()}); + return t.person_age_year_months(months: ageInMonths - 12); } else { - return "person_age_years".t(args: {'years': ageInYears.toString()}); + return t.person_age_years(years: ageInYears); } } @@ -33,7 +34,7 @@ int _calculateAgeInMonths(DateTime birthDate, DateTime referenceDate) { (referenceDate.day < birthDate.day ? 1 : 0); } -Future showNameEditModal(BuildContext context, DriftPerson person) { +Future showNameEditModal(BuildContext context, Person person) { return showDialog( context: context, useRootNavigator: false, @@ -43,7 +44,7 @@ Future showNameEditModal(BuildContext context, DriftPerson person) { ); } -Future showBirthdayEditModal(BuildContext context, DriftPerson person) { +Future showBirthdayEditModal(BuildContext context, Person person) { return showDialog( context: context, useRootNavigator: false, diff --git a/mobile/lib/widgets/activities/dismissible_activity.dart b/mobile/lib/widgets/activities/dismissible_activity.dart index c056f5ee35..f1305ba3aa 100644 --- a/mobile/lib/widgets/activities/dismissible_activity.dart +++ b/mobile/lib/widgets/activities/dismissible_activity.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; class DismissibleActivity extends StatelessWidget { @@ -23,9 +24,9 @@ class DismissibleActivity extends StatelessWidget { context: context, builder: (context) => ConfirmDialog( onOk: () {}, - title: "shared_album_activity_remove_title", - content: "shared_album_activity_remove_content", - ok: "delete", + title: context.t.shared_album_activity_remove_title, + content: context.t.shared_album_activity_remove_content, + ok: context.t.delete, ), ) : Future.value(false), diff --git a/mobile/lib/widgets/asset_grid/delete_dialog.dart b/mobile/lib/widgets/asset_grid/delete_dialog.dart index ff5aac617a..9b0367a463 100644 --- a/mobile/lib/widgets/asset_grid/delete_dialog.dart +++ b/mobile/lib/widgets/asset_grid/delete_dialog.dart @@ -1,69 +1,69 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; - -class DeleteLocalOnlyDialog extends StatelessWidget { - final void Function(bool onlyMerged) onDeleteLocal; - - const DeleteLocalOnlyDialog({super.key, required this.onDeleteLocal}); - - @override - Widget build(BuildContext context) { - void onDeleteBackedUpOnly() { - context.pop(true); - onDeleteLocal(true); - } - - void onForceDelete() { - context.pop(false); - onDeleteLocal(false); - } - - return AlertDialog( - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - title: const Text("delete_dialog_title").tr(), - content: const Text("delete_dialog_alert_local_non_backed_up").tr(), - actions: [ - SizedBox( - width: double.infinity, - height: 48, - child: FilledButton( - onPressed: () => context.pop(), - style: FilledButton.styleFrom( - backgroundColor: context.colorScheme.surfaceDim, - foregroundColor: context.primaryColor, - ), - child: const Text("cancel", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ), - const SizedBox(height: 8), - SizedBox( - width: double.infinity, - height: 48, - - child: FilledButton( - onPressed: onDeleteBackedUpOnly, - style: FilledButton.styleFrom( - backgroundColor: context.colorScheme.errorContainer, - foregroundColor: context.colorScheme.onErrorContainer, - ), - child: const Text( - "delete_local_dialog_ok_backed_up_only", - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(), - ), - ), - const SizedBox(height: 8), - SizedBox( - width: double.infinity, - height: 48, - child: FilledButton( - onPressed: onForceDelete, - style: FilledButton.styleFrom(backgroundColor: Colors.red[400], foregroundColor: Colors.white), - child: const Text("delete_local_dialog_ok_force", style: TextStyle(fontWeight: FontWeight.bold)).tr(), - ), - ), - ], - ); - } -} +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; + +class DeleteLocalOnlyDialog extends StatelessWidget { + final void Function(bool onlyMerged) onDeleteLocal; + + const DeleteLocalOnlyDialog({super.key, required this.onDeleteLocal}); + + @override + Widget build(BuildContext context) { + void onDeleteBackedUpOnly() { + context.pop(true); + onDeleteLocal(true); + } + + void onForceDelete() { + context.pop(false); + onDeleteLocal(false); + } + + return AlertDialog( + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), + title: Text(context.t.delete_dialog_title), + content: Text(context.t.delete_dialog_alert_local_non_backed_up), + actions: [ + SizedBox( + width: double.infinity, + height: 48, + child: FilledButton( + onPressed: () => context.pop(), + style: FilledButton.styleFrom( + backgroundColor: context.colorScheme.surfaceDim, + foregroundColor: context.primaryColor, + ), + child: Text(context.t.cancel, style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + height: 48, + + child: FilledButton( + onPressed: onDeleteBackedUpOnly, + style: FilledButton.styleFrom( + backgroundColor: context.colorScheme.errorContainer, + foregroundColor: context.colorScheme.onErrorContainer, + ), + child: Text( + context.t.delete_local_dialog_ok_backed_up_only, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + height: 48, + child: FilledButton( + onPressed: onForceDelete, + style: FilledButton.styleFrom(backgroundColor: Colors.red[400], foregroundColor: Colors.white), + child: Text(context.t.delete_local_dialog_ok_force, style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/cast_dialog.dart b/mobile/lib/widgets/asset_viewer/cast_dialog.dart index f91d8f5b27..70b1b3d3e5 100644 --- a/mobile/lib/widgets/asset_viewer/cast_dialog.dart +++ b/mobile/lib/widgets/asset_viewer/cast_dialog.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; @@ -23,7 +23,7 @@ class CastDialog extends ConsumerWidget { } return AlertDialog( - title: const Text("cast", style: TextStyle(fontWeight: FontWeight.bold)).tr(), + title: Text(context.t.cast, style: const TextStyle(fontWeight: FontWeight.bold)), content: SizedBox( width: 250, height: 250, @@ -31,13 +31,13 @@ class CastDialog extends ConsumerWidget { future: ref.watch(castProvider.notifier).getDevices(), builder: (context, snapshot) { if (snapshot.hasError) { - return Text('error_saving_image'.tr(args: [snapshot.error.toString()])); + return Text(context.t.error_saving_image(error: snapshot.error.toString())); } else if (!snapshot.hasData) { return const SizedBox(height: 48, child: Center(child: CircularProgressIndicator())); } if (snapshot.data!.isEmpty) { - return const Text('no_cast_devices_found').tr(); + return Text(context.t.no_cast_devices_found); } final devices = snapshot.data!; @@ -47,12 +47,12 @@ class CastDialog extends ConsumerWidget { final List sectionedList = []; if (connected.isNotEmpty) { - sectionedList.add("connected_device"); + sectionedList.add(context.t.connected_device); sectionedList.addAll(connected); } if (others.isNotEmpty) { - sectionedList.add("discovered_devices"); + sectionedList.add(context.t.discovered_devices); sectionedList.addAll(others); } @@ -66,7 +66,7 @@ class CastDialog extends ConsumerWidget { // It's a section header return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text(item, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)).tr(), + child: Text(item, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), ); } else { final (deviceName, type, deviceObj) = item as (String, CastDestinationType, dynamic); @@ -110,16 +110,16 @@ class CastDialog extends ConsumerWidget { TextButton( onPressed: () => ref.read(castProvider.notifier).disconnect(), child: Text( - "stop_casting", + context.t.stop_casting, style: TextStyle(color: context.colorScheme.secondary, fontWeight: FontWeight.bold), - ).tr(), + ), ), TextButton( onPressed: () => context.pop(), child: Text( - "close", + context.t.close, style: TextStyle(color: context.colorScheme.primary, fontWeight: FontWeight.bold), - ).tr(), + ), ), ], ); diff --git a/mobile/lib/widgets/backup/backup_info_card.dart b/mobile/lib/widgets/backup/backup_info_card.dart index 7911679577..4efd940e36 100644 --- a/mobile/lib/widgets/backup/backup_info_card.dart +++ b/mobile/lib/widgets/backup/backup_info_card.dart @@ -1,8 +1,7 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class BackupInfoCard extends StatelessWidget { final String title; @@ -73,11 +72,11 @@ class BackupInfoCard extends StatelessWidget { ], ), Text( - "backup_info_card_assets", + context.t.backup_info_card_assets, style: context.textTheme.labelLarge?.copyWith( color: context.colorScheme.onSurface.withAlpha(isLoading ? 50 : 255), ), - ).tr(), + ), ], ), ), @@ -93,7 +92,7 @@ class BackupInfoCard extends StatelessWidget { ), onTap: onTap, title: Text( - "view_details".t(context: context), + context.t.view_details, style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)), ), trailing: Icon(Icons.arrow_forward_ios, size: 16, color: context.colorScheme.onSurfaceVariant), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index b390d2a423..fffa6746f6 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -1,11 +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' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; @@ -78,20 +78,24 @@ class ImmichAppBarDialog extends HookConsumerWidget { title: Text( text, style: theme.textTheme.labelLarge?.copyWith(color: theme.textTheme.labelLarge?.color?.withAlpha(250)), - ).tr(), + ), onTap: onTap, trailing: trailing, ); } ListTile buildSettingButton() { - return buildActionButton(Icons.settings_outlined, "settings", () => context.pushRoute(const SettingsRoute())); + return buildActionButton( + Icons.settings_outlined, + context.t.settings, + () => context.pushRoute(const SettingsRoute()), + ); } ListTile buildFreeUpSpaceButton() { return buildActionButton( Icons.cleaning_services_outlined, - "free_up_space", + context.t.free_up_space, () => context.pushRoute(SettingsSubRoute(section: SettingSection.freeUpSpace)), ); } @@ -99,7 +103,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ListTile buildAppLogButton() { return buildActionButton( Icons.assignment_outlined, - "profile_drawer_app_logs", + context.t.profile_drawer_app_logs, () => context.pushRoute(const AppLogRoute()), ); } @@ -107,7 +111,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ListTile buildSignOutButton() { return buildActionButton( Icons.logout_rounded, - "sign_out", + context.t.sign_out, () async { if (isLoggingOut.value) { return; @@ -118,9 +122,9 @@ class ImmichAppBarDialog extends HookConsumerWidget { context: context, builder: (BuildContext ctx) { return ConfirmDialog( - title: "app_bar_signout_dialog_title", - content: "app_bar_signout_dialog_content", - ok: "yes", + title: context.t.app_bar_signout_dialog_title, + content: context.t.app_bar_signout_dialog_content, + ok: context.t.yes, onOk: () async { isLoggingOut.value = true; await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false); @@ -163,16 +167,16 @@ class ImmichAppBarDialog extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, spacing: 12, children: [ - Text("backup_controller_page_server_storage".tr(), style: context.textTheme.labelLarge), + Text(context.t.backup_controller_page_server_storage, style: context.textTheme.labelLarge), LinearProgressIndicator( minHeight: 10.0, value: percentage, borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), Text( - 'backup_controller_page_storage_format', + context.t.backup_controller_page_storage_format(used: usedDiskSpace, total: totalDiskSpace), style: context.textTheme.bodySmall, - ).tr(namedArgs: {'used': usedDiskSpace, 'total': totalDiskSpace}), + ), ], ), ); @@ -189,7 +193,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ContextHelper(context).pop(); unawaited(launchUrl(Uri.parse('https://docs.immich.app'), mode: LaunchMode.externalApplication)); }, - child: Text("documentation", style: context.textTheme.bodySmall).tr(), + child: Text(context.t.documentation, style: context.textTheme.bodySmall), ), const SizedBox(width: 20, child: Text("â€ĸ", textAlign: TextAlign.center)), InkWell( @@ -199,7 +203,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { launchUrl(Uri.parse('https://github.com/immich-app/immich'), mode: LaunchMode.externalApplication), ); }, - child: Text("profile_drawer_github", style: context.textTheme.bodySmall).tr(), + child: Text(context.t.profile_drawer_github, style: context.textTheme.bodySmall), ), const SizedBox(width: 20, child: Text("â€ĸ", textAlign: TextAlign.center)), InkWell( @@ -219,7 +223,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { applicationVersion: packageInfo.version, ); }, - child: Text("licenses", style: context.textTheme.bodySmall).tr(), + child: Text(context.t.licenses, style: context.textTheme.bodySmall), ), ], ), @@ -237,10 +241,10 @@ class ImmichAppBarDialog extends HookConsumerWidget { minLeadingWidth: 20, tileColor: theme.primaryColor.withAlpha(80), title: Text( - "profile_drawer_readonly_mode", + context.t.profile_drawer_readonly_mode, style: theme.textTheme.labelLarge?.copyWith(color: theme.textTheme.labelLarge?.color?.withAlpha(250)), textAlign: TextAlign.center, - ).tr(), + ), ), ); } diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index 51cdd7c5a3..bd8e883ef0 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; @@ -68,7 +68,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { SnackBar( duration: const Duration(seconds: 2), content: Text( - (isReadonlyModeEnabled ? "readonly_mode_disabled" : "readonly_mode_enabled").tr(), + isReadonlyModeEnabled ? context.t.readonly_mode_disabled : context.t.readonly_mode_enabled, style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), ), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index 348e3aa14c..901ffb2496 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -46,20 +46,20 @@ class AppBarServerInfo extends HookConsumerWidget { children: [ if (showVersionWarning) ...[const ServerUpdateNotification(), divider], _ServerInfoItem( - label: "server_info_box_app_version".tr(), + label: context.t.server_info_box_app_version, text: "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}", ), divider, _ServerInfoItem( - label: "server_version".tr(), + label: context.t.server_version, text: serverInfoState.serverVersion.major > 0 ? "${serverInfoState.serverVersion}" : "--", ), divider, - _ServerInfoItem(label: "server_info_box_server_url".tr(), text: getServerUrl() ?? '--', tooltip: true), + _ServerInfoItem(label: context.t.server_info_box_server_url, text: getServerUrl() ?? '--', tooltip: true), if (serverInfoState.latestVersion != null) ...[ divider, _ServerInfoItem( - label: "latest_version".tr(), + label: context.t.latest_version, text: serverInfoState.latestVersion!.major > 0 ? "${serverInfoState.latestVersion!}" : "--", tooltip: true, icon: serverInfoState.versionStatus == VersionStatus.serverOutOfDate diff --git a/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart b/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart index 806ac87c19..8261f71ad8 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'dart:io'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -76,8 +76,8 @@ class ServerUpdateNotification extends HookConsumerWidget { tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: serverInfoState.versionStatus == VersionStatus.clientOutOfDate - ? Text("action_common_update".tr(context: context)) - : Text("view".tr()), + ? Text(context.t.action_common_update) + : Text(context.t.view), ), ], ], diff --git a/mobile/lib/widgets/common/confirm_dialog.dart b/mobile/lib/widgets/common/confirm_dialog.dart index 9c3f4344c6..74b7f4a04c 100644 --- a/mobile/lib/widgets/common/confirm_dialog.dart +++ b/mobile/lib/widgets/common/confirm_dialog.dart @@ -1,22 +1,15 @@ -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/generated/translations.g.dart'; class ConfirmDialog extends StatelessWidget { final Function? onOk; final String title; final String content; - final String cancel; - final String ok; + final String? cancel; + final String? ok; - const ConfirmDialog({ - super.key, - this.onOk, - required this.title, - required this.content, - this.cancel = "cancel", - this.ok = "backup_controller_page_background_battery_info_ok", - }); + const ConfirmDialog({super.key, this.onOk, required this.title, required this.content, this.cancel, this.ok}); @override Widget build(BuildContext context) { @@ -27,22 +20,22 @@ class ConfirmDialog extends StatelessWidget { return AlertDialog( shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - title: Text(title).tr(), - content: Text(content).tr(), + title: Text(title), + content: Text(content), actions: [ TextButton( onPressed: () => context.pop(false), child: Text( - cancel, + cancel ?? context.t.cancel, style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold), - ).tr(), + ), ), TextButton( onPressed: onOkPressed, child: Text( - ok, + ok ?? context.t.backup_controller_page_background_battery_info_ok, style: TextStyle(color: context.colorScheme.error, fontWeight: FontWeight.bold), - ).tr(), + ), ), ], ); diff --git a/mobile/lib/widgets/common/date_time_picker.dart b/mobile/lib/widgets/common/date_time_picker.dart index 3aedd55de5..86154c308e 100644 --- a/mobile/lib/widgets/common/date_time_picker.dart +++ b/mobile/lib/widgets/common/date_time_picker.dart @@ -1,10 +1,11 @@ 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:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/duration_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/widgets/common/dropdown_search_menu.dart'; +import 'package:intl/intl.dart'; import 'package:timezone/timezone.dart' as tz; import 'package:timezone/timezone.dart'; @@ -116,26 +117,26 @@ class _DateTimePicker extends HookWidget { TextButton( onPressed: () => context.pop(), child: Text( - "cancel", + context.t.cancel, style: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: context.colorScheme.error, ), - ).tr(), + ), ), TextButton( onPressed: popWithDateTime, child: Text( - "action_common_update", + context.t.action_common_update, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), - ).tr(), + ), ), ], content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text("date_and_time", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)).tr(), + Text(context.t.date_and_time, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 32), ListTile( tileColor: context.colorScheme.surfaceContainerHighest, @@ -151,8 +152,8 @@ class _DateTimePicker extends HookWidget { const SizedBox(height: 24), DropdownSearchMenu( trailingIcon: Icon(Icons.arrow_drop_down, color: context.primaryColor), - hintText: "timezone".tr(), - label: const Text('timezone').tr(), + hintText: context.t.timezone, + label: Text(context.t.timezone), textStyle: context.textTheme.bodyMedium, onSelected: (value) => tzOffset.value = value, initialSelection: tzOffset.value, diff --git a/mobile/lib/widgets/common/dropdown_search_menu.dart b/mobile/lib/widgets/common/dropdown_search_menu.dart index 54568c392a..ba40bb98d5 100644 --- a/mobile/lib/widgets/common/dropdown_search_menu.dart +++ b/mobile/lib/widgets/common/dropdown_search_menu.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class DropdownSearchMenu extends HookWidget { const DropdownSearchMenu({ @@ -81,7 +81,7 @@ class DropdownSearchMenu extends HookWidget { autofocus: true, focusNode: focusNode, controller: textEditingController, - decoration: inputDecoration.copyWith(hintText: "search_timezone".tr()), + decoration: inputDecoration.copyWith(hintText: context.t.search_timezone), maxLines: 1, style: context.textTheme.bodyMedium, expands: false, diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 54495365db..6a119f8ca3 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -2,12 +2,12 @@ import 'dart:async'; import 'dart:math' as math; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; @@ -120,7 +120,7 @@ class _ProfileIndicator extends ConsumerWidget { SnackBar( duration: const Duration(seconds: 2), content: Text( - (isReadonlyModeEnabled ? "readonly_mode_disabled" : "readonly_mode_enabled").tr(), + isReadonlyModeEnabled ? context.t.readonly_mode_disabled : context.t.readonly_mode_enabled, style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), ), @@ -143,7 +143,7 @@ class _ProfileIndicator extends ConsumerWidget { ? context.colorScheme.error : context.primaryColor, size: widgetSize / 2 - 3, - semanticLabel: 'new_version_available'.tr(), + semanticLabel: context.t.new_version_available, ), ), backgroundColor: Colors.transparent, @@ -153,7 +153,7 @@ class _ProfileIndicator extends ConsumerWidget { child: user == null ? const Icon(Icons.face_outlined, size: widgetSize) : Semantics( - label: "logged_in_as".tr(namedArgs: {"user": user.name}), + label: context.t.logged_in_as(user: user.name), child: AbsorbPointer( child: Builder( builder: (context) => UserCircleAvatar( @@ -177,7 +177,15 @@ class _BackupIndicator extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final indicatorIcon = _getBackupBadgeIcon(context, ref); + final backupEnabled = ref.watch(appConfigProvider.select((c) => c.backup.enabled)); + final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none)); + final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty)); + final indicatorIcon = _getBackupBadgeIcon( + context, + backupEnabled: backupEnabled, + hasError: hasError, + isUploading: isUploading, + ); return IconButton( onPressed: () => context.pushRoute(const DriftBackupRoute()), @@ -192,16 +200,23 @@ class _BackupIndicator extends ConsumerWidget { ); } - Widget? _getBackupBadgeIcon(BuildContext context, WidgetRef ref) { - final backupEnabled = ref.read(appConfigProvider.select((c) => c.backup.enabled)); - final hasError = ref.read(driftBackupProvider.select((state) => state.error != BackupError.none)); + Widget? _getBackupBadgeIcon( + BuildContext context, { + required bool backupEnabled, + required bool hasError, + required bool isUploading, + }) { final isDarkTheme = context.isDarkTheme; final iconColor = isDarkTheme ? Colors.white : Colors.black; - final isUploading = ref.read(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty)); if (!backupEnabled) { return _BadgeLabel( - Icon(Icons.cloud_off_rounded, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()), + Icon( + Icons.cloud_off_rounded, + size: 9, + color: iconColor, + semanticLabel: context.t.backup_controller_page_backup, + ), ); } @@ -211,7 +226,7 @@ class _BackupIndicator extends ConsumerWidget { Icons.warning_rounded, size: 12, color: context.colorScheme.error, - semanticLabel: 'backup_controller_page_backup'.tr(), + semanticLabel: context.t.backup_controller_page_backup, ), backgroundColor: context.colorScheme.errorContainer, ); @@ -229,7 +244,7 @@ class _BackupIndicator extends ConsumerWidget { strokeWidth: 2, strokeCap: StrokeCap.round, valueColor: AlwaysStoppedAnimation(iconColor), - semanticsLabel: 'backup_controller_page_backup'.tr(), + semanticsLabel: context.t.backup_controller_page_backup, ), ), ), @@ -237,7 +252,7 @@ class _BackupIndicator extends ConsumerWidget { } return _BadgeLabel( - Icon(Icons.check_outlined, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()), + Icon(Icons.check_outlined, size: 9, color: iconColor, semanticLabel: context.t.backup_controller_page_backup), ); } } diff --git a/mobile/lib/widgets/common/local_album_sliver_app_bar.dart b/mobile/lib/widgets/common/local_album_sliver_app_bar.dart index 85d8f4bb01..1de1d1a262 100644 --- a/mobile/lib/widgets/common/local_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/local_album_sliver_app_bar.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class LocalAlbumsSliverAppBar extends StatelessWidget { const LocalAlbumsSliverAppBar({super.key}); @@ -15,7 +15,7 @@ class LocalAlbumsSliverAppBar extends StatelessWidget { shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), automaticallyImplyLeading: true, centerTitle: true, - title: Text("on_this_device".t(context: context)), + title: Text(context.t.on_this_device), ); } } diff --git a/mobile/lib/widgets/common/location_picker.dart b/mobile/lib/widgets/common/location_picker.dart index c7eb827781..cf3240977b 100644 --- a/mobile/lib/widgets/common/location_picker.dart +++ b/mobile/lib/widgets/common/location_picker.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:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -73,11 +73,11 @@ class _LocationPicker extends HookWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("edit_location_dialog_title", style: context.textTheme.titleMedium).tr(), + Text(context.t.edit_location_dialog_title, style: context.textTheme.titleMedium), Align( alignment: Alignment.center, child: TextButton.icon( - icon: const Text("location_picker_choose_on_map").tr(), + icon: Text(context.t.location_picker_choose_on_map), label: const Icon(Icons.map_outlined, size: 16), onPressed: onMapTap, ), @@ -85,9 +85,9 @@ class _LocationPicker extends HookWidget { const SizedBox(height: 12), _ManualPickerInput( controller: latitudeController, - decorationText: "latitude", - hintText: "location_picker_latitude_hint", - errorText: "location_picker_latitude_error", + decorationText: context.t.latitude, + hintText: context.t.location_picker_latitude_hint, + errorText: context.t.location_picker_latitude_error, focusNode: latitiudeFocusNode, validator: _validateLat, onUpdated: onLatitudeUpdated, @@ -95,9 +95,9 @@ class _LocationPicker extends HookWidget { const SizedBox(height: 24), _ManualPickerInput( controller: longitudeController, - decorationText: "longitude", - hintText: "location_picker_longitude_hint", - errorText: "location_picker_longitude_error", + decorationText: context.t.longitude, + hintText: context.t.location_picker_longitude_hint, + errorText: context.t.location_picker_longitude_error, focusNode: longitudeFocusNode, validator: _validateLong, onUpdated: onLongitudeEditingCompleted, @@ -109,19 +109,19 @@ class _LocationPicker extends HookWidget { TextButton( onPressed: () => ContextHelper(context).pop(), child: Text( - "cancel", + context.t.cancel, style: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: context.colorScheme.error, ), - ).tr(), + ), ), TextButton( onPressed: () => context.maybePop(latlng), child: Text( - "action_common_update", + context.t.action_common_update, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), - ).tr(), + ), ), ], ); @@ -163,13 +163,13 @@ class _ManualPickerInput extends HookWidget { textInputAction: TextInputAction.done, autofocus: false, decoration: InputDecoration( - labelText: decorationText.tr(), + labelText: decorationText, labelStyle: TextStyle(fontWeight: FontWeight.bold, color: context.primaryColor), floatingLabelBehavior: FloatingLabelBehavior.auto, border: const OutlineInputBorder(), - hintText: hintText.tr(), + hintText: hintText, hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14), - errorText: isValid.value ? null : errorText.tr(), + errorText: isValid.value ? null : errorText, ), onEditingComplete: onEditingComplete, keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), diff --git a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart index 0ceb98aa76..ac53bca72f 100644 --- a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart @@ -8,7 +8,7 @@ import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @@ -237,7 +237,7 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> { final assetCount = ref.watch(timelineServiceProvider.select((s) => s.totalAssets)); return Text( - 'items_count'.t(context: context, args: {"count": assetCount}), + context.t.items_count(count: assetCount), style: context.textTheme.labelLarge?.copyWith( // letterSpacing: 0.2, fontWeight: FontWeight.bold, diff --git a/mobile/lib/widgets/common/person_sliver_app_bar.dart b/mobile/lib/widgets/common/person_sliver_app_bar.dart index 0e6829243c..56eb3c6094 100644 --- a/mobile/lib/widgets/common/person_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/person_sliver_app_bar.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; @@ -28,7 +28,7 @@ class PersonSliverAppBar extends ConsumerStatefulWidget { required this.onBirthdayTap, }); - final DriftPerson person; + final Person person; final VoidCallback onNameTap; final VoidCallback onBirthdayTap; final VoidCallback onShowOptions; @@ -137,7 +137,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState { class _ExpandedBackground extends ConsumerStatefulWidget { final double scrollProgress; - final DriftPerson person; + final Person person; final VoidCallback onNameTap; final VoidCallback onBirthdayTap; @@ -261,7 +261,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S ), ) : Text( - 'add_a_name'.tr(), + context.t.add_a_name, style: context.textTheme.titleLarge?.copyWith( color: Colors.grey[400], fontSize: 36, @@ -293,7 +293,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S ) else Text( - 'add_birthday'.tr(), + context.t.add_birthday, style: context.textTheme.labelLarge?.copyWith( color: Colors.grey[400], height: 1.2, @@ -344,7 +344,7 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> { final assetCount = ref.watch(timelineServiceProvider.select((s) => s.totalAssets)); return Text( - 'items_count'.t(context: context, args: {"count": assetCount}), + context.t.items_count(count: assetCount), style: context.textTheme.labelLarge?.copyWith( fontWeight: FontWeight.bold, color: Colors.white, diff --git a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart index 09a0367039..4b9532eb88 100644 --- a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/datetime_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart'; @@ -318,7 +318,7 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> { final assetCount = ref.watch(timelineServiceProvider.select((s) => s.totalAssets)); return Text( - 'items_count'.t(context: context, args: {"count": assetCount}), + context.t.items_count(count: assetCount), style: context.textTheme.labelLarge?.copyWith( color: Colors.white, shadows: [const Shadow(offset: Offset(0, 2), blurRadius: 12, color: Colors.black87)], diff --git a/mobile/lib/widgets/common/scaffold_error_body.dart b/mobile/lib/widgets/common/scaffold_error_body.dart index 2e2d8fb506..5f3fb1cb0d 100644 --- a/mobile/lib/widgets/common/scaffold_error_body.dart +++ b/mobile/lib/widgets/common/scaffold_error_body.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; // Error widget to be used in Scaffold when an AsyncError is received class ScaffoldErrorBody extends StatelessWidget { @@ -15,7 +15,11 @@ class ScaffoldErrorBody extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("scaffold_body_error_occurred", style: context.textTheme.displayMedium, textAlign: TextAlign.center).tr(), + Text( + context.t.scaffold_body_error_occurred, + style: context.textTheme.displayMedium, + textAlign: TextAlign.center, + ), if (withIcon) Center( child: Padding( diff --git a/mobile/lib/widgets/common/selection_sliver_app_bar.dart b/mobile/lib/widgets/common/selection_sliver_app_bar.dart index d60124041c..f6fb2fc88f 100644 --- a/mobile/lib/widgets/common/selection_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/selection_sliver_app_bar.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; class SelectionSliverAppBar extends ConsumerStatefulWidget { @@ -43,12 +43,12 @@ class _SelectionSliverAppBarState extends ConsumerState { }, ), centerTitle: true, - title: Text("Select {count}".t(context: context, args: {'count': filteredAssets.length.toString()})), + title: Text(context.t.selected_count(count: filteredAssets.length)), actions: [ TextButton( onPressed: () => onDone(filteredAssets), child: Text( - 'done'.t(context: context), + context.t.done, style: context.textTheme.titleSmall?.copyWith(color: context.colorScheme.primary), ), ), diff --git a/mobile/lib/widgets/common/tag_picker.dart b/mobile/lib/widgets/common/tag_picker.dart index 82228f5b0b..2b4f07536f 100644 --- a/mobile/lib/widgets/common/tag_picker.dart +++ b/mobile/lib/widgets/common/tag_picker.dart @@ -1,10 +1,10 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/tag.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/tag.provider.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @@ -41,19 +41,19 @@ class _TagPickerModal extends HookWidget { TextButton( onPressed: () => context.pop(), child: Text( - "cancel", + context.t.cancel, style: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: context.colorScheme.error, ), - ).tr(), + ), ), TextButton( onPressed: () => context.pop((selectedTagIds.value, newTagValues.value)), child: Text( - "action_common_update", + context.t.action_common_update, style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600, color: context.primaryColor), - ).tr(), + ), ), ], content: SizedBox( @@ -98,7 +98,7 @@ class TagPicker extends HookConsumerWidget { onChanged: (value) => searchQuery.value = value, onTapOutside: (_) => formFocus.unfocus(), filled: true, - hintText: 'filter_tags'.tr(), + hintText: context.t.filter_tags, ), ), Padding( diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 229eaa7ec6..65a4c6f88e 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.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_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/generated/translations.g.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -29,13 +29,13 @@ class ChangePasswordForm extends HookConsumerWidget { alignment: WrapAlignment.start, children: [ Text( - 'change_password'.tr(), + context.t.change_password, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: context.primaryColor), ), Padding( padding: const EdgeInsets.symmetric(vertical: 24.0), child: Text( - 'change_password_form_description'.tr(namedArgs: {'name': authState.name}), + context.t.change_password_form_description(name: authState.name), style: TextStyle(fontSize: 14, color: context.colorScheme.onSurface, fontWeight: FontWeight.w600), ), ), @@ -62,7 +62,7 @@ class ChangePasswordForm extends HookConsumerWidget { if (!isSuccess && context.mounted) { ImmichToast.show( context: context, - msg: "login_password_changed_error".tr(), + msg: context.t.login_password_changed_error, toastType: ToastType.error, gravity: ToastGravity.TOP, ); @@ -82,7 +82,7 @@ class ChangePasswordForm extends HookConsumerWidget { AutoRouter.of(context).back(); ImmichToast.show( context: context, - msg: "login_password_changed_success".tr(), + msg: context.t.login_password_changed_success, toastType: ToastType.success, gravity: ToastGravity.TOP, ); @@ -92,7 +92,7 @@ class ChangePasswordForm extends HookConsumerWidget { TextButton.icon( icon: const Icon(Icons.arrow_back), onPressed: () => AutoRouter.of(context).back(), - label: const Text('back').tr(), + label: Text(context.t.back), ), ], ), @@ -116,9 +116,9 @@ class PasswordInput extends StatelessWidget { obscureText: true, controller: controller, decoration: InputDecoration( - labelText: 'change_password_form_new_password'.tr(), + labelText: context.t.change_password_form_new_password, border: const OutlineInputBorder(), - hintText: 'change_password_form_new_password'.tr(), + hintText: context.t.change_password_form_new_password, ), ); } @@ -132,7 +132,7 @@ class ConfirmPasswordInput extends StatelessWidget { String? _validateInput(String? email) { if (confirmController.value != originalController.value) { - return 'change_password_form_password_mismatch'.tr(); + return StaticTranslations.instance.change_password_form_password_mismatch; } return null; } @@ -143,8 +143,8 @@ class ConfirmPasswordInput extends StatelessWidget { obscureText: true, controller: confirmController, decoration: InputDecoration( - labelText: 'change_password_form_confirm_password'.tr(), - hintText: 'change_password_form_reenter_new_password'.tr(), + labelText: context.t.change_password_form_confirm_password, + hintText: context.t.change_password_form_reenter_new_password, border: const OutlineInputBorder(), ), validator: _validateInput, @@ -166,7 +166,7 @@ class ChangePasswordButton extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), ), onPressed: onPressed, - child: Text('change_password'.tr(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), + child: Text(context.t.change_password, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), ); } } diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index e4c7516c43..5185c5c318 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -5,7 +5,6 @@ import 'dart:math'; import 'package:auto_route/auto_route.dart'; import 'package:crypto/crypto.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; @@ -14,7 +13,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; @@ -43,20 +42,21 @@ class LoginForm extends HookConsumerWidget { final log = Logger('LoginForm'); - String? _validateUrl(String? url) => normalizeAndValidateServerUrl(url) ? null : 'login_form_err_invalid_url'.tr(); + String? _validateUrl(String? url) => + normalizeAndValidateServerUrl(url) ? null : StaticTranslations.instance.login_form_err_invalid_url; String? _validateEmail(String? email) { if (email == null || email == '') { return null; } if (email.endsWith(' ')) { - return 'login_form_err_trailing_whitespace'.tr(); + return StaticTranslations.instance.login_form_err_trailing_whitespace; } if (email.startsWith(' ')) { - return 'login_form_err_leading_whitespace'.tr(); + return StaticTranslations.instance.login_form_err_leading_whitespace; } if (email.contains(' ') || !email.contains('@')) { - return 'login_form_err_invalid_email'.tr(); + return StaticTranslations.instance.login_form_err_invalid_email; } return null; } @@ -96,7 +96,7 @@ class LoginForm extends HookConsumerWidget { // Guard empty URL if (serverUrl.isEmpty) { - ImmichToast.show(context: context, msg: "login_form_server_empty".tr(), toastType: ToastType.error); + ImmichToast.show(context: context, msg: context.t.login_form_server_empty, toastType: ToastType.error); } try { @@ -127,7 +127,7 @@ class LoginForm extends HookConsumerWidget { ImmichToast.show( context: context, - msg: e.message ?? 'login_form_api_exception'.tr(), + msg: e.message ?? context.t.login_form_api_exception, toastType: ToastType.error, gravity: ToastGravity.TOP, ); @@ -140,7 +140,7 @@ class LoginForm extends HookConsumerWidget { ImmichToast.show( context: context, - msg: 'login_form_handshake_exception'.tr(), + msg: context.t.login_form_handshake_exception, toastType: ToastType.error, gravity: ToastGravity.TOP, ); @@ -153,7 +153,7 @@ class LoginForm extends HookConsumerWidget { ImmichToast.show( context: context, - msg: 'login_form_server_error'.tr(), + msg: context.t.login_form_server_error, toastType: ToastType.error, gravity: ToastGravity.TOP, ); @@ -210,15 +210,15 @@ class LoginForm extends HookConsumerWidget { shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), elevation: 5, title: Text( - 'manage_media_access_title', + context.t.manage_media_access_title, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: context.primaryColor), - ).tr(), + ), content: SingleChildScrollView( child: ListBody( children: [ - const Text('manage_media_access_subtitle', style: TextStyle(fontSize: 14)).tr(), + Text(context.t.manage_media_access_subtitle, style: const TextStyle(fontSize: 14)), const SizedBox(height: 4), - const Text('manage_media_access_rationale', style: TextStyle(fontSize: 12)).tr(), + Text(context.t.manage_media_access_rationale, style: const TextStyle(fontSize: 12)), ], ), ), @@ -226,7 +226,7 @@ class LoginForm extends HookConsumerWidget { TextButton( onPressed: () => Navigator.of(context).pop(), child: Text( - 'cancel'.tr(), + context.t.cancel, style: TextStyle(fontWeight: FontWeight.w600, color: context.primaryColor), ), ), @@ -236,7 +236,7 @@ class LoginForm extends HookConsumerWidget { Navigator.of(context).pop(); }, child: Text( - 'manage_media_access_settings'.tr(), + context.t.manage_media_access_settings, style: TextStyle(fontWeight: FontWeight.w600, color: context.primaryColor), ), ), @@ -290,7 +290,7 @@ class LoginForm extends HookConsumerWidget { ImmichToast.show( context: context, - msg: "login_form_failed_login".tr(), + msg: context.t.login_form_failed_login, toastType: ToastType.error, gravity: ToastGravity.TOP, ); @@ -348,7 +348,7 @@ class LoginForm extends HookConsumerWidget { ImmichToast.show( context: context, - msg: "login_form_failed_get_oauth_server_config".tr(), + msg: context.t.login_form_failed_get_oauth_server_config, toastType: ToastType.error, gravity: ToastGravity.TOP, ); @@ -404,7 +404,7 @@ class LoginForm extends HookConsumerWidget { ImmichToast.show( context: context, - msg: "login_form_failed_get_oauth_server_disable".tr(), + msg: context.t.login_form_failed_get_oauth_server_disable, toastType: ToastType.info, gravity: ToastGravity.TOP, ); @@ -451,19 +451,19 @@ class LoginForm extends HookConsumerWidget { children: [ ImmichForm( onSubmit: getServerAuthSettings, - submitText: 'next'.t(context: context), + submitText: context.t.next, submitIcon: Icons.arrow_forward_rounded, builder: (_, form) => ImmichURLInput( controller: serverEndpointController, - label: 'login_form_endpoint_url'.t(context: context), - hintText: 'login_form_endpoint_hint'.t(context: context), + label: context.t.login_form_endpoint_url, + hintText: context.t.login_form_endpoint_hint, validator: _validateUrl, keyboardAction: .next, onSubmit: (_) => form.submit(), ), ), ImmichTextButton( - labelText: 'settings'.t(context: context), + labelText: context.t.settings, icon: Icons.settings, variant: ImmichVariant.ghost, onPressed: () => context.pushRoute(const SettingsRoute()), @@ -488,15 +488,15 @@ class LoginForm extends HookConsumerWidget { if (isPasswordLoginEnable.value) ImmichForm( onSubmit: login, - submitText: 'login'.t(context: context), + submitText: context.t.login, submitIcon: Icons.login_rounded, builder: (context, form) => Column( spacing: ImmichSpacing.md, children: [ ImmichTextInput( controller: emailController, - label: 'email'.t(context: context), - hintText: 'login_form_email_hint'.t(context: context), + label: context.t.email, + hintText: context.t.login_form_email_hint, validator: _validateEmail, keyboardAction: TextInputAction.next, keyboardType: TextInputType.emailAddress, @@ -506,8 +506,8 @@ class LoginForm extends HookConsumerWidget { ImmichPasswordInput( controller: passwordController, focusNode: passwordFocusNode, - label: 'password'.t(context: context), - hintText: 'login_form_password_hint'.t(context: context), + label: context.t.password, + hintText: context.t.login_form_password_hint, keyboardAction: TextInputAction.go, onSubmit: (_) => form.submit(), ), @@ -526,10 +526,9 @@ class LoginForm extends HookConsumerWidget { ) : const SizedBox.shrink(), ), - if (!isOauthEnable.value && !isPasswordLoginEnable.value) - Center(child: const Text('login_disabled').tr()), + if (!isOauthEnable.value && !isPasswordLoginEnable.value) Center(child: Text(context.t.login_disabled)), ImmichTextButton( - labelText: 'back'.t(context: context), + labelText: context.t.back, icon: Icons.arrow_back, variant: ImmichVariant.ghost, onPressed: () => serverEndpoint.value = null, diff --git a/mobile/lib/widgets/forms/pin_registration_form.dart b/mobile/lib/widgets/forms/pin_registration_form.dart index ebd299bcce..bc14388021 100644 --- a/mobile/lib/widgets/forms/pin_registration_form.dart +++ b/mobile/lib/widgets/forms/pin_registration_form.dart @@ -1,8 +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/generated/translations.g.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/widgets/forms/pin_input.dart'; @@ -58,7 +58,7 @@ class PinRegistrationForm extends HookConsumerWidget { SizedBox( width: context.width * 0.7, child: Text( - 'setup_pin_code'.tr(), + context.t.setup_pin_code, style: context.textTheme.labelLarge!.copyWith(fontSize: 24), textAlign: TextAlign.center, ), @@ -66,7 +66,7 @@ class PinRegistrationForm extends HookConsumerWidget { SizedBox( width: context.width * 0.8, child: Text( - 'new_pin_code_subtitle'.tr(), + context.t.new_pin_code_subtitle, style: context.textTheme.bodyLarge!.copyWith(fontSize: 16), textAlign: TextAlign.center, ), @@ -74,7 +74,7 @@ class PinRegistrationForm extends HookConsumerWidget { const SizedBox(height: 32), PinInput( controller: newPinCodeController, - label: 'new_pin_code'.tr(), + label: context.t.new_pin_code, length: 6, autoFocus: true, hasError: hasError.value, @@ -87,7 +87,7 @@ class PinRegistrationForm extends HookConsumerWidget { const SizedBox(height: 32), PinInput( controller: confirmPinCodeController, - label: 'confirm_new_pin_code'.tr(), + label: context.t.confirm_new_pin_code, length: 6, hasError: hasError.value, onChanged: (input) { @@ -102,7 +102,7 @@ class PinRegistrationForm extends HookConsumerWidget { child: Row( children: [ Expanded( - child: ElevatedButton(onPressed: createNewPinCode, child: Text('create'.tr())), + child: ElevatedButton(onPressed: createNewPinCode, child: Text(context.t.create)), ), ], ), diff --git a/mobile/lib/widgets/forms/pin_verification_form.dart b/mobile/lib/widgets/forms/pin_verification_form.dart index de2acaa1f7..6f1ba25fd0 100644 --- a/mobile/lib/widgets/forms/pin_verification_form.dart +++ b/mobile/lib/widgets/forms/pin_verification_form.dart @@ -1,8 +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/generated/translations.g.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/widgets/forms/pin_input.dart'; @@ -60,7 +60,7 @@ class PinVerificationForm extends HookConsumerWidget { SizedBox( width: context.width * 0.7, child: Text( - description ?? 'enter_your_pin_code_subtitle'.tr(), + description ?? context.t.enter_your_pin_code_subtitle, style: context.textTheme.labelLarge!.copyWith(fontSize: 18), textAlign: TextAlign.center, ), diff --git a/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart b/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart index 2a4dacaff7..eeebc28dda 100644 --- a/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart +++ b/mobile/lib/widgets/map/map_settings/map_settings_time_dropdown.dart @@ -1,7 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class MapTimeDropDown extends StatelessWidget { final int relativeTime; @@ -20,7 +19,7 @@ class MapTimeDropDown extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "date_range".t(context: context), + context.t.date_range, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), Flexible( @@ -30,24 +29,21 @@ class MapTimeDropDown extends StatelessWidget { initialSelection: relativeTime, onSelected: (value) => onTimeChange(value!), dropdownMenuEntries: [ - DropdownMenuEntry(value: 0, label: "all".t(context: context)), - DropdownMenuEntry(value: 1, label: "map_settings_date_range_option_day".t(context: context)), - DropdownMenuEntry(value: 7, label: "map_settings_date_range_option_days".tr(namedArgs: {'days': "7"})), - DropdownMenuEntry( - value: 30, - label: "map_settings_date_range_option_days".tr(namedArgs: {'days': "30"}), - ), + DropdownMenuEntry(value: 0, label: context.t.all), + DropdownMenuEntry(value: 1, label: context.t.map_settings_date_range_option_day), + DropdownMenuEntry(value: 7, label: context.t.map_settings_date_range_option_days(days: 7)), + DropdownMenuEntry(value: 30, label: context.t.map_settings_date_range_option_days(days: 30)), DropdownMenuEntry( value: now .difference(DateTime(now.year - 1, now.month, now.day, now.hour, now.minute, now.second)) .inDays, - label: "map_settings_date_range_option_year".t(context: context), + label: context.t.map_settings_date_range_option_year, ), DropdownMenuEntry( value: now .difference(DateTime(now.year - 3, now.month, now.day, now.hour, now.minute, now.second)) .inDays, - label: "map_settings_date_range_option_years".t(args: {'years': "3"}), + label: context.t.map_settings_date_range_option_years(years: 3), ), ], ), diff --git a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart index e66f08c221..ec83d5085e 100644 --- a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart +++ b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -18,7 +18,7 @@ class MapThemePicker extends StatelessWidget { padding: const EdgeInsets.only(bottom: 20), child: Center( child: Text( - "map_settings_theme_settings".t(context: context), + context.t.map_settings_theme_settings, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), ), diff --git a/mobile/lib/widgets/memories/memory_epilogue.dart b/mobile/lib/widgets/memories/memory_epilogue.dart index b866ed049a..0335b586ac 100644 --- a/mobile/lib/widgets/memories/memory_epilogue.dart +++ b/mobile/lib/widgets/memories/memory_epilogue.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class MemoryEpilogue extends StatefulWidget { final Function()? onStartOver; @@ -45,23 +45,23 @@ class _MemoryEpilogueState extends State with TickerProviderStat ), const SizedBox(height: 16.0), Text( - "memories_all_caught_up", + context.t.memories_all_caught_up, style: context.textTheme.headlineMedium?.copyWith(color: Colors.white), - ).tr(), + ), const SizedBox(height: 16.0), Text( - "memories_check_back_tomorrow", + context.t.memories_check_back_tomorrow, style: context.textTheme.bodyMedium?.copyWith(color: Colors.white), - ).tr(), + ), const SizedBox(height: 16.0), TextButton( onPressed: widget.onStartOver, child: Text( - "memories_start_over", + context.t.memories_start_over, style: context.textTheme.displayMedium?.copyWith( color: context.isDarkTheme ? context.colorScheme.primary : context.colorScheme.inversePrimary, ), - ).tr(), + ), ), ], ), @@ -85,9 +85,9 @@ class _MemoryEpilogueState extends State with TickerProviderStat ), ), Text( - "memories_swipe_to_close", + context.t.memories_swipe_to_close, style: context.textTheme.bodyMedium?.copyWith(color: Colors.white), - ).tr(), + ), ], ), ), diff --git a/mobile/lib/widgets/search/search_filter/camera_picker.dart b/mobile/lib/widgets/search/search_filter/camera_picker.dart index 6a025bdb69..aad5fda18f 100644 --- a/mobile/lib/widgets/search/search_filter/camera_picker.dart +++ b/mobile/lib/widgets/search/search_filter/camera_picker.dart @@ -1,8 +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/generated/translations.g.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/providers/search/search_filter.provider.dart'; import 'package:immich_mobile/widgets/search/search_filter/common/dropdown.dart'; @@ -21,7 +21,9 @@ class CameraPicker extends HookConsumerWidget { final selectedMake = useState(filter?.make); final selectedModel = useState(filter?.model); - final make = ref.watch(getSearchSuggestionsProvider(SearchSuggestionArgs(type: SearchSuggestionType.cameraMake))); + final make = ref.watch( + getSearchSuggestionsProvider(const SearchSuggestionArgs(type: SearchSuggestionType.cameraMake)), + ); final models = ref.watch( getSearchSuggestionsProvider( @@ -35,7 +37,7 @@ class CameraPicker extends HookConsumerWidget { AsyncData(:final value) => value.map((e) => DropdownMenuEntry(value: e, label: e)).toList(), _ => [], }, - label: const Text('make').tr(), + label: Text(context.t.make), controller: makeTextController, leadingIcon: const Icon(Icons.photo_camera_rounded), onSelected: (value) { @@ -54,7 +56,7 @@ class CameraPicker extends HookConsumerWidget { AsyncData(:final value) => value.map((e) => DropdownMenuEntry(value: e, label: e)).toList(), _ => [], }, - label: const Text('model').tr(), + label: Text(context.t.model), controller: modelTextController, leadingIcon: const Icon(Icons.camera), onSelected: (value) { diff --git a/mobile/lib/widgets/search/search_filter/display_option_picker.dart b/mobile/lib/widgets/search/search_filter/display_option_picker.dart index a64eab7b71..83ddc98107 100644 --- a/mobile/lib/widgets/search/search_filter/display_option_picker.dart +++ b/mobile/lib/widgets/search/search_filter/display_option_picker.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; enum DisplayOption { notInAlbum, favorite, archive } @@ -23,7 +23,7 @@ class DisplayOptionPicker extends HookWidget { shrinkWrap: true, children: [ CheckboxListTile( - title: const Text('search_filter_display_option_not_in_album').tr(), + title: Text(context.t.search_filter_display_option_not_in_album), value: options.value[DisplayOption.notInAlbum], onChanged: (bool? value) { options.value = {...options.value, DisplayOption.notInAlbum: value!}; @@ -31,7 +31,7 @@ class DisplayOptionPicker extends HookWidget { }, ), CheckboxListTile( - title: const Text('favorite').tr(), + title: Text(context.t.favorite), value: options.value[DisplayOption.favorite], onChanged: (value) { options.value = {...options.value, DisplayOption.favorite: value!}; @@ -39,7 +39,7 @@ class DisplayOptionPicker extends HookWidget { }, ), CheckboxListTile( - title: const Text('archive').tr(), + title: Text(context.t.archive), value: options.value[DisplayOption.archive], onChanged: (value) { options.value = {...options.value, DisplayOption.archive: value!}; diff --git a/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart b/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart index 1c338b8828..a74ea41c38 100644 --- a/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart +++ b/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class FilterBottomSheetScaffold extends StatelessWidget { const FilterBottomSheetScaffold({ @@ -46,7 +46,7 @@ class FilterBottomSheetScaffold extends StatelessWidget { onClear(); context.pop(); }, - child: const Text('clear').tr(), + child: Text(context.t.clear), ), if (onSearch != null) const SizedBox(width: 8), if (onSearch != null) @@ -56,7 +56,7 @@ class FilterBottomSheetScaffold extends StatelessWidget { onSearch!(); context.pop(); }, - child: const Text('search_filter_apply').tr(), + child: Text(context.t.search_filter_apply), ), ], ), diff --git a/mobile/lib/widgets/search/search_filter/location_picker.dart b/mobile/lib/widgets/search/search_filter/location_picker.dart index f521a50f35..ab35145a28 100644 --- a/mobile/lib/widgets/search/search_filter/location_picker.dart +++ b/mobile/lib/widgets/search/search_filter/location_picker.dart @@ -1,7 +1,7 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/providers/search/search_filter.provider.dart'; import 'package:immich_mobile/widgets/search/search_filter/common/dropdown.dart'; @@ -61,7 +61,7 @@ class LocationPicker extends HookConsumerWidget { AsyncData(:final value) => value.map((e) => DropdownMenuEntry(value: e, label: e)).toList(), _ => [], }, - label: const Text('country').tr(), + label: Text(context.t.country), controller: countryTextController, onSelected: (value) { if (value.toString() == selectedCountry.value) { @@ -80,7 +80,7 @@ class LocationPicker extends HookConsumerWidget { AsyncData(:final value) => value.map((e) => DropdownMenuEntry(value: e, label: e)).toList(), _ => [], }, - label: const Text('state').tr(), + label: Text(context.t.state), controller: stateTextController, onSelected: (value) { if (value.toString() == selectedState.value) { @@ -98,7 +98,7 @@ class LocationPicker extends HookConsumerWidget { AsyncData(:final value) => value.map((e) => DropdownMenuEntry(value: e, label: e)).toList(), _ => [], }, - label: const Text('city').tr(), + label: Text(context.t.city), controller: cityTextController, onSelected: (value) { selectedCity.value = value.toString(); diff --git a/mobile/lib/widgets/search/search_filter/media_type_picker.dart b/mobile/lib/widgets/search/search_filter/media_type_picker.dart index ac89de8190..dd0767a97a 100644 --- a/mobile/lib/widgets/search/search_filter/media_type_picker.dart +++ b/mobile/lib/widgets/search/search_filter/media_type_picker.dart @@ -1,7 +1,7 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class MediaTypePicker extends HookWidget { const MediaTypePicker({super.key, required this.onSelect, this.filter}); @@ -21,9 +21,9 @@ class MediaTypePicker extends HookWidget { groupValue: selectedMediaType.value, child: Column( children: [ - RadioListTile(key: const Key("all"), title: const Text("all").tr(), value: AssetType.other), - RadioListTile(key: const Key("image"), title: const Text("image").tr(), value: AssetType.image), - RadioListTile(key: const Key("video"), title: const Text("video").tr(), value: AssetType.video), + RadioListTile(key: const Key("all"), title: Text(context.t.all), value: AssetType.other), + RadioListTile(key: const Key("image"), title: Text(context.t.image), value: AssetType.image), + RadioListTile(key: const Key("video"), title: Text(context.t.video), value: AssetType.video), ], ), ); diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index 0732b30a63..ee738265b2 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -1,112 +1,112 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/person.model.dart'; -import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; -import 'package:immich_mobile/pages/common/large_leading_tile.dart'; -import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; -import 'package:immich_mobile/providers/search/people.provider.dart'; -import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:immich_mobile/widgets/common/search_field.dart'; - -class PeoplePicker extends HookConsumerWidget { - const PeoplePicker({super.key, required this.onSelect, this.filter}); - - final Function(Set) onSelect; - final Set? filter; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final formFocus = useFocusNode(); - const imageSize = 60.0; - final searchQuery = useState(''); - final people = ref.watch(getAllPeopleProvider); - final selectedPeople = useState>(filter ?? {}); - - return Column( - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: SearchField( - focusNode: formFocus, - onChanged: (value) => searchQuery.value = value, - onTapOutside: (_) => formFocus.unfocus(), - filled: true, - hintText: 'filter_people'.tr(), - ), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 0), - child: Divider(color: context.colorScheme.surfaceContainerHighest, thickness: 1), - ), - Expanded( - child: people.widgetWhen( - onData: (people) { - final filtered = people - .where( - (person) => person.name.toLowerCase().removeDiacritics().contains( - searchQuery.value.toLowerCase().removeDiacritics(), - ), - ) - .toList(); - return ListView.builder( - shrinkWrap: true, - itemCount: filtered.length, - padding: const EdgeInsets.all(8), - itemBuilder: (context, index) { - final person = filtered[index]; - final isSelected = selectedPeople.value.contains(person); - - return Padding( - key: ValueKey(person.id), - padding: const EdgeInsets.only(bottom: 2.0), - child: LargeLeadingTile( - title: Text( - person.name, - style: context.textTheme.bodyLarge?.copyWith( - fontSize: 20, - fontWeight: FontWeight.w500, - color: isSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface, - ), - ), - leading: SizedBox( - height: imageSize, - child: Material( - shape: const CircleBorder(side: BorderSide.none), - elevation: 3, - child: CircleAvatar( - key: ValueKey(person.id), - maxRadius: imageSize / 2, - backgroundImage: RemoteImageProvider( - url: getFaceThumbnailUrl(person.id, updatedAt: person.updatedAt), - ), - ), - ), - ), - onTap: () { - if (selectedPeople.value.contains(person)) { - selectedPeople.value.remove(person); - } else { - selectedPeople.value.add(person); - } - - selectedPeople.value = {...selectedPeople.value}; - onSelect(selectedPeople.value); - }, - selected: isSelected, - selectedTileColor: context.primaryColor, - tileColor: context.primaryColor.withAlpha(25), - ), - ); - }, - ); - }, - ), - ), - ], - ); - } -} +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/person.model.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/string_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/pages/common/large_leading_tile.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/widgets/common/search_field.dart'; + +class PeoplePicker extends HookConsumerWidget { + const PeoplePicker({super.key, required this.onSelect, this.filter}); + + final Function(Set) onSelect; + final Set? filter; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formFocus = useFocusNode(); + const imageSize = 60.0; + final searchQuery = useState(''); + final people = ref.watch(getAllPeopleProvider); + final selectedPeople = useState>(filter ?? {}); + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: SearchField( + focusNode: formFocus, + onChanged: (value) => searchQuery.value = value, + onTapOutside: (_) => formFocus.unfocus(), + filled: true, + hintText: context.t.filter_people, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 0), + child: Divider(color: context.colorScheme.surfaceContainerHighest, thickness: 1), + ), + Expanded( + child: people.widgetWhen( + onData: (people) { + final filtered = people + .where( + (person) => person.name.toLowerCase().removeDiacritics().contains( + searchQuery.value.toLowerCase().removeDiacritics(), + ), + ) + .toList(); + return ListView.builder( + shrinkWrap: true, + itemCount: filtered.length, + padding: const EdgeInsets.all(8), + itemBuilder: (context, index) { + final person = filtered[index]; + final isSelected = selectedPeople.value.contains(person); + + return Padding( + key: ValueKey(person.id), + padding: const EdgeInsets.only(bottom: 2.0), + child: LargeLeadingTile( + title: Text( + person.name.nullIfEmpty ?? context.t.no_name, + style: context.textTheme.bodyLarge?.copyWith( + fontSize: 20, + fontWeight: FontWeight.w500, + color: isSelected ? context.colorScheme.onPrimary : context.colorScheme.onSurface, + ), + ), + leading: SizedBox( + height: imageSize, + child: Material( + shape: const CircleBorder(side: BorderSide.none), + elevation: 3, + child: CircleAvatar( + key: ValueKey(person.id), + maxRadius: imageSize / 2, + backgroundImage: RemoteImageProvider( + url: getFaceThumbnailUrl(person.id, updatedAt: person.updatedAt), + ), + ), + ), + ), + onTap: () { + if (selectedPeople.value.contains(person)) { + selectedPeople.value.remove(person); + } else { + selectedPeople.value.add(person); + } + + selectedPeople.value = {...selectedPeople.value}; + onSelect(selectedPeople.value); + }, + selected: isSelected, + selectedTileColor: context.primaryColor, + tileColor: context.primaryColor.withAlpha(25), + ), + ); + }, + ); + }, + ), + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/search/search_filter/star_rating_picker.dart b/mobile/lib/widgets/search/search_filter/star_rating_picker.dart index 864ddf2591..643f4f167c 100644 --- a/mobile/lib/widgets/search/search_filter/star_rating_picker.dart +++ b/mobile/lib/widgets/search/search_filter/star_rating_picker.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/utils/option.dart'; @@ -28,7 +28,7 @@ class StarRatingPicker extends HookWidget { 6, (index) => RadioListTile( key: Key("star_$index"), - title: Text('rating_count'.t(args: {'count': index})), + title: Text(context.t.rating_count(count: index)), value: index, ), ), diff --git a/mobile/lib/widgets/search/search_row_title.dart b/mobile/lib/widgets/search/search_row_title.dart index dc0a4ba6cb..cf4cc62c18 100644 --- a/mobile/lib/widgets/search/search_row_title.dart +++ b/mobile/lib/widgets/search/search_row_title.dart @@ -1,6 +1,6 @@ -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; class SearchRowTitle extends StatelessWidget { const SearchRowTitle({super.key, required this.onViewAllPressed, required this.title}); @@ -17,9 +17,9 @@ class SearchRowTitle extends StatelessWidget { TextButton( onPressed: onViewAllPressed, child: Text( - 'search_page_view_all_button', + context.t.search_page_view_all_button, style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor), - ).tr(), + ), ), ], ); diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index cb035a22e9..9e80f8715c 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -2,12 +2,12 @@ import 'dart:async'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; @@ -71,8 +71,8 @@ class AdvancedSettings extends HookConsumerWidget { SettingsSwitchListTile( enabled: true, valueNotifier: advancedTroubleshooting, - title: "advanced_settings_troubleshooting_title".tr(), - subtitle: "advanced_settings_troubleshooting_subtitle".tr(), + title: context.t.advanced_settings_troubleshooting_title, + subtitle: context.t.advanced_settings_troubleshooting_subtitle, ), if (isManageMediaSupported.value) Column( @@ -80,8 +80,8 @@ class AdvancedSettings extends HookConsumerWidget { SettingsSwitchListTile( enabled: true, valueNotifier: manageLocalMediaAndroid, - title: "advanced_settings_sync_remote_deletions_title".tr(), - subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(), + title: context.t.advanced_settings_sync_remote_deletions_title, + subtitle: context.t.advanced_settings_sync_remote_deletions_subtitle, onChanged: (value) async { if (value) { final result = await ref.read(permissionRepositoryProvider).requestManageMediaPermission(); @@ -91,9 +91,9 @@ class AdvancedSettings extends HookConsumerWidget { }, ), SettingsActionTile( - title: "manage_media_access_title".tr(), - statusText: manageMediaAndroidPermission.value ? "allowed".tr() : "not_allowed".tr(), - subtitle: "manage_media_access_rationale".tr(), + title: context.t.manage_media_access_title, + statusText: manageMediaAndroidPermission.value ? context.t.allowed : context.t.not_allowed, + subtitle: context.t.manage_media_access_rationale, statusColor: manageLocalMediaAndroid.value && !manageMediaAndroidPermission.value ? const Color.fromARGB(255, 243, 188, 106) : null, @@ -105,7 +105,7 @@ class AdvancedSettings extends HookConsumerWidget { ], ), SettingsSliderListTile( - text: "advanced_settings_log_level_title".tr(namedArgs: {'level': logLevel}), + text: context.t.advanced_settings_log_level_title(level: logLevel), valueNotifier: levelId, maxValue: 8, minValue: 1, @@ -114,15 +114,15 @@ class AdvancedSettings extends HookConsumerWidget { ), SettingsSwitchListTile( valueNotifier: preferRemote, - title: "advanced_settings_prefer_remote_title".tr(), - subtitle: "advanced_settings_prefer_remote_subtitle".tr(), + title: context.t.advanced_settings_prefer_remote_title, + subtitle: context.t.advanced_settings_prefer_remote_subtitle, ), const CustomProxyHeaderSettings(), const SslClientCertSettings(), SettingsSwitchListTile( valueNotifier: readonlyModeEnabled, - title: "advanced_settings_readonly_mode_title".tr(), - subtitle: "advanced_settings_readonly_mode_subtitle".tr(), + title: context.t.advanced_settings_readonly_mode_title, + subtitle: context.t.advanced_settings_readonly_mode_subtitle, onChanged: (value) { readonlyModeEnabled.value = value; ref.read(readonlyModeProvider.notifier).setReadonlyMode(value); @@ -130,7 +130,7 @@ class AdvancedSettings extends HookConsumerWidget { SnackBar( duration: const Duration(seconds: 2), content: Text( - (value ? "readonly_mode_enabled" : "readonly_mode_disabled").tr(), + value ? context.t.readonly_mode_enabled : context.t.readonly_mode_disabled, style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), ), @@ -138,7 +138,7 @@ class AdvancedSettings extends HookConsumerWidget { }, ), ListTile( - title: Text("advanced_settings_clear_image_cache".tr(), style: const TextStyle(fontWeight: FontWeight.w500)), + title: Text(context.t.advanced_settings_clear_image_cache, style: const TextStyle(fontWeight: FontWeight.w500)), leading: const Icon(Icons.playlist_remove_rounded), onTap: () async { final int clearedBytes; @@ -153,7 +153,7 @@ class AdvancedSettings extends HookConsumerWidget { SnackBar( duration: const Duration(seconds: 2), content: Text( - "advanced_settings_clear_image_cache_error".tr(), + context.t.advanced_settings_clear_image_cache_error, style: context.textTheme.bodyLarge?.copyWith(color: context.themeData.colorScheme.error), ), ), @@ -171,7 +171,7 @@ class AdvancedSettings extends HookConsumerWidget { SnackBar( duration: const Duration(seconds: 2), content: Text( - "advanced_settings_clear_image_cache_success".tr(namedArgs: {'size': clearedMB}), + context.t.advanced_settings_clear_image_cache_success(size: clearedMB), style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), ), ), diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart index a5ced2ef5f..a68ad0fe5e 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_group_settings.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; @@ -34,20 +34,14 @@ class GroupSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle( - title: "asset_list_group_by_sub_title".t(context: context), - icon: Icons.group_work_outlined, - ), + SettingGroupTitle(title: context.t.asset_list_group_by_sub_title, icon: Icons.group_work_outlined), SettingsRadioListTile( groups: [ SettingsRadioGroup( - title: 'asset_list_layout_settings_group_by_month_day'.t(context: context), + title: context.t.asset_list_layout_settings_group_by_month_day, value: GroupAssetsBy.day, ), - SettingsRadioGroup( - title: 'month'.t(context: context), - value: GroupAssetsBy.month, - ), + SettingsRadioGroup(title: context.t.month, value: GroupAssetsBy.month), ], groupBy: groupBy.value, onRadioChanged: changeGroupValue, diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart index 5b4c4bb89f..6ecd76dd78 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -1,10 +1,9 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; @@ -23,13 +22,10 @@ class LayoutSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle( - title: "asset_list_layout_sub_title".t(context: context), - icon: Icons.view_module_outlined, - ), + SettingGroupTitle(title: context.t.asset_list_layout_sub_title, icon: Icons.view_module_outlined), SettingsSliderListTile( valueNotifier: tilesPerRow, - text: 'theme_setting_asset_list_tiles_per_row_title'.tr(namedArgs: {'count': "${tilesPerRow.value}"}), + text: context.t.theme_setting_asset_list_tiles_per_row_title(count: tilesPerRow.value), label: "${tilesPerRow.value}", maxValue: 6, minValue: 2, diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart index 1f3f4bbcf0..08fe897c5f 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart @@ -1,9 +1,9 @@ import 'dart:async'; -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/generated/translations.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_group_settings.dart'; @@ -21,7 +21,7 @@ class AssetListSettings extends HookConsumerWidget { final assetListSetting = [ SettingsSwitchListTile( valueNotifier: storageIndicator, - title: 'theme_setting_asset_list_storage_indicator_title'.tr(), + title: context.t.theme_setting_asset_list_storage_indicator_title, onChanged: (value) { unawaited(ref.read(settingsProvider).write(.timelineStorageIndicator, value)); ref.invalidate(appSettingsServiceProvider); diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart index 82a2d804a3..d1b1e3d0f2 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; @@ -23,14 +23,14 @@ class ImageViewerQualitySetting extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ SettingGroupTitle( - title: "photos".t(context: context), + title: context.t.photos, icon: Icons.image_outlined, - subtitle: "setting_image_viewer_help".t(context: context), + subtitle: context.t.setting_image_viewer_help, ), SettingsSwitchListTile( valueNotifier: isOriginal, - title: "setting_image_viewer_original_title".t(context: context), - subtitle: "setting_image_viewer_original_subtitle".t(context: context), + title: context.t.setting_image_viewer_original_title, + subtitle: context.t.setting_image_viewer_original_subtitle, onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), ], diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart index fc3466c3c6..ed07028bcf 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart @@ -1,9 +1,9 @@ import 'dart:async'; -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/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -21,11 +21,11 @@ class ImageViewerTapToNavigateSetting extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsSubTitle(title: "setting_image_navigation_title".tr()), + SettingsSubTitle(title: context.t.setting_image_navigation_title), SettingsSwitchListTile( valueNotifier: tapToNavigate, - title: "setting_image_navigation_enable_title".tr(), - subtitle: "setting_image_navigation_enable_subtitle".tr(), + title: context.t.setting_image_navigation_enable_title, + subtitle: context.t.setting_image_navigation_enable_subtitle, ), ], ); diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart index d8ccd4f0ed..bc4d9a1235 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart'; @@ -39,40 +39,28 @@ class SlideshowSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle( - title: 'slideshow'.t(context: context), - icon: Icons.slideshow_outlined, - ), + SettingGroupTitle(title: context.t.slideshow, icon: Icons.slideshow_outlined), SettingsSwitchListTile( valueNotifier: useRepeat, - title: "slideshow_repeat".t(context: context), - subtitle: "slideshow_repeat_description".t(context: context), + title: context.t.slideshow_repeat, + subtitle: context.t.slideshow_repeat_description, ), SettingsSliderListTile( valueNotifier: useDuration, - text: "duration".t(context: context), + text: context.t.duration, minValue: 5, noDivisons: 5, maxValue: 30, ), Padding( padding: const EdgeInsets.only(top: 20), - child: SettingsSubTitle(title: 'look'.t(context: context)), + child: SettingsSubTitle(title: context.t.look), ), SettingsRadioListTile( groups: [ - SettingsRadioGroup( - title: 'contain'.t(context: context), - value: SlideshowLook.contain, - ), - SettingsRadioGroup( - title: 'cover'.t(context: context), - value: SlideshowLook.cover, - ), - SettingsRadioGroup( - title: 'blurred_background'.t(context: context), - value: SlideshowLook.blurredBackground, - ), + SettingsRadioGroup(title: context.t.contain, value: SlideshowLook.contain), + SettingsRadioGroup(title: context.t.cover, value: SlideshowLook.cover), + SettingsRadioGroup(title: context.t.blurred_background, value: SlideshowLook.blurredBackground), ], groupBy: useLook.value, onRadioChanged: (value) { @@ -83,24 +71,15 @@ class SlideshowSettings extends HookConsumerWidget { ), Padding( padding: const EdgeInsets.only(top: 20), - child: SettingsSubTitle(title: 'direction'.t(context: context)), + child: SettingsSubTitle(title: context.t.direction), ), Padding( padding: const EdgeInsets.only(bottom: 32), child: SettingsRadioListTile( groups: [ - SettingsRadioGroup( - title: 'forward'.t(context: context), - value: SlideshowDirection.forward, - ), - SettingsRadioGroup( - title: 'backward'.t(context: context), - value: SlideshowDirection.backward, - ), - SettingsRadioGroup( - title: 'shuffle'.t(context: context), - value: SlideshowDirection.shuffle, - ), + SettingsRadioGroup(title: context.t.forward, value: SlideshowDirection.forward), + SettingsRadioGroup(title: context.t.backward, value: SlideshowDirection.backward), + SettingsRadioGroup(title: context.t.shuffle, value: SlideshowDirection.shuffle), ], groupBy: useDirection.value, onRadioChanged: (value) { diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart index 7bea439f0c..2a575061ca 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -31,24 +31,21 @@ class VideoViewerSettings extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle( - title: "videos".t(context: context), - icon: Icons.video_camera_back_outlined, - ), + SettingGroupTitle(title: context.t.videos, icon: Icons.video_camera_back_outlined), SettingsSwitchListTile( valueNotifier: useAutoPlayVideo, - title: "setting_video_viewer_auto_play_title".t(context: context), - subtitle: "setting_video_viewer_auto_play_subtitle".t(context: context), + title: context.t.setting_video_viewer_auto_play_title, + subtitle: context.t.setting_video_viewer_auto_play_subtitle, ), SettingsSwitchListTile( valueNotifier: useLoopVideo, - title: "setting_video_viewer_looping_title".t(context: context), - subtitle: "loop_videos_description".t(context: context), + title: context.t.setting_video_viewer_looping_title, + subtitle: context.t.loop_videos_description, ), SettingsSwitchListTile( valueNotifier: useOriginalVideo, - title: "setting_video_viewer_original_video_title".t(context: context), - subtitle: "setting_video_viewer_original_video_subtitle".t(context: context), + title: context.t.setting_video_viewer_original_video_title, + subtitle: context.t.setting_video_viewer_original_video_subtitle, ), ], ); diff --git a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart index e7d56ed66a..ae9ff87a91 100644 --- a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart @@ -8,7 +8,6 @@ import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; @@ -26,26 +25,17 @@ class DriftBackupSettings extends StatelessWidget { Widget build(BuildContext context) { return SettingsSubPageScaffold( settings: [ - SettingGroupTitle( - title: "network_requirements".t(context: context), - icon: Icons.cell_tower, - ), + SettingGroupTitle(title: context.t.network_requirements, icon: Icons.cell_tower), const _UseCellularForVideosButton(), const _UseCellularForPhotosButton(), if (CurrentPlatform.isAndroid) ...[ const Divider(), - SettingGroupTitle( - title: "background_options".t(context: context), - icon: Icons.charging_station_rounded, - ), + SettingGroupTitle(title: context.t.background_options, icon: Icons.charging_station_rounded), const _BackupOnlyWhenChargingButton(), const _BackupDelaySlider(), ], const Divider(), - SettingGroupTitle( - title: "backup_albums_sync".t(context: context), - icon: Icons.sync, - ), + SettingGroupTitle(title: context.t.backup_albums_sync, icon: Icons.sync), const _AlbumSyncActionButton(), ], ); @@ -108,8 +98,8 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> Column( children: [ SettingListTile( - title: "sync_albums".t(context: context), - subtitle: "sync_upload_album_setting_subtitle".t(context: context), + title: context.t.sync_albums, + subtitle: context.t.sync_upload_album_setting_subtitle, trailing: Switch( value: albumSyncEnable, onChanged: (bool newValue) async { @@ -131,8 +121,8 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> ? SettingListTile( onTap: _manualSyncAlbums, contentPadding: const EdgeInsets.only(left: 32, right: 16), - title: "organize_into_albums".t(context: context), - subtitle: "organize_into_albums_description".t(context: context), + title: context.t.organize_into_albums, + subtitle: context.t.organize_into_albums_description, trailing: isAlbumSyncInProgress ? const SizedBox( width: 32, @@ -161,15 +151,15 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton> class _BackupSwitchTile extends ConsumerWidget { final SettingsKey metadataKey; final bool Function(AppConfig) selector; - final String titleKey; - final String subtitleKey; + final String title; + final String subtitle; final void Function(bool)? onChanged; const _BackupSwitchTile({ required this.metadataKey, required this.selector, - required this.titleKey, - required this.subtitleKey, + required this.title, + required this.subtitle, this.onChanged, }); @@ -179,8 +169,8 @@ class _BackupSwitchTile extends ConsumerWidget { return Padding( padding: const EdgeInsets.only(left: 8.0), child: SettingListTile( - title: titleKey.t(context: context), - subtitle: subtitleKey.t(context: context), + title: title, + subtitle: subtitle, trailing: Switch( value: value, onChanged: (bool newValue) async { @@ -201,8 +191,8 @@ class _UseCellularForVideosButton extends StatelessWidget { return _BackupSwitchTile( metadataKey: SettingsKey.backupUseCellularForVideos, selector: (c) => c.backup.useCellularForVideos, - titleKey: "videos", - subtitleKey: "network_requirement_videos_upload", + title: context.t.videos, + subtitle: context.t.network_requirement_videos_upload, ); } } @@ -215,8 +205,8 @@ class _UseCellularForPhotosButton extends StatelessWidget { return _BackupSwitchTile( metadataKey: SettingsKey.backupUseCellularForPhotos, selector: (c) => c.backup.useCellularForPhotos, - titleKey: "photos", - subtitleKey: "network_requirement_photos_upload", + title: context.t.photos, + subtitle: context.t.network_requirement_photos_upload, ); } } @@ -230,8 +220,8 @@ class _BackupOnlyWhenChargingButton extends ConsumerWidget { return _BackupSwitchTile( metadataKey: SettingsKey.backupRequireCharging, selector: (c) => c.backup.requireCharging, - titleKey: "charging", - subtitleKey: "charging_requirement_mobile_backup", + title: context.t.charging, + subtitle: context.t.charging_requirement_mobile_backup, onChanged: (value) { unawaited(fgService.configure(requireCharging: value)); }, diff --git a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart index 097f72e1e1..d2edef832e 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; @@ -45,9 +44,7 @@ class SyncStatusAndActions extends HookConsumerWidget { return; } - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Database file not found".t(context: context))), - ); + context.scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Database file not found'))); return; } @@ -77,17 +74,13 @@ class SyncStatusAndActions extends HookConsumerWidget { return; } - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Database exported successfully".t(context: context))), - ); + context.scaffoldMessenger.showSnackBar(const SnackBar(content: Text('Database exported successfully'))); } catch (e) { if (!context.mounted) { return; } - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Failed to export database: $e".t(context: context))), - ); + context.scaffoldMessenger.showSnackBar(SnackBar(content: Text('Failed to export database: $e'))); } } @@ -138,10 +131,10 @@ class SyncStatusAndActions extends HookConsumerWidget { const _SyncStatsCounts(), const Divider(height: 10), const SizedBox(height: 16), - SettingGroupTitle(title: "jobs".t(context: context)), + SettingGroupTitle(title: context.t.jobs), SettingListTile( - title: "sync_local".t(context: context), - subtitle: "tap_to_run_job".t(context: context), + title: context.t.sync_local, + subtitle: context.t.tap_to_run_job, leading: const Icon(Icons.sync), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).localSyncStatus), onTap: () { @@ -149,8 +142,8 @@ class SyncStatusAndActions extends HookConsumerWidget { }, ), SettingListTile( - title: "sync_remote".t(context: context), - subtitle: "tap_to_run_job".t(context: context), + title: context.t.sync_remote, + subtitle: context.t.tap_to_run_job, leading: const Icon(Icons.cloud_sync), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).remoteSyncStatus), onTap: () { @@ -159,16 +152,16 @@ class SyncStatusAndActions extends HookConsumerWidget { ), if (CurrentPlatform.isIOS && serverVersion.isAtLeast(major: 2, minor: 5)) SettingListTile( - title: "Sync Cloud Ids".t(context: context), + title: 'Sync Cloud Ids', leading: const Icon(Icons.cloud_circle_rounded), - subtitle: "tap_to_run_job".t(context: context), + subtitle: context.t.tap_to_run_job, trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).cloudIdSyncStatus), onTap: ref.watch(backgroundSyncProvider).syncCloudIds, ), SettingListTile( - title: "hash_asset".t(context: context), + title: context.t.hash_asset, leading: const Icon(Icons.tag), - subtitle: "tap_to_run_job".t(context: context), + subtitle: context.t.tap_to_run_job, trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).hashJobStatus), onTap: () { unawaited(ref.read(backgroundSyncProvider).hashAssets()); @@ -176,27 +169,21 @@ class SyncStatusAndActions extends HookConsumerWidget { ), const Divider(height: 1), const SizedBox(height: 16), - SettingGroupTitle(title: "actions".t(context: context)), + SettingGroupTitle(title: context.t.actions), ListTile( - title: Text( - "clear_file_cache".t(context: context), - style: const TextStyle(fontWeight: FontWeight.w500), - ), + title: Text(context.t.clear_file_cache, style: const TextStyle(fontWeight: FontWeight.w500)), leading: const Icon(Icons.playlist_remove_rounded), onTap: clearFileCache, ), ListTile( - title: Text( - "export_database".t(context: context), - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text("export_database_description".t(context: context)), + title: Text(context.t.export_database, style: const TextStyle(fontWeight: FontWeight.w500)), + subtitle: Text(context.t.export_database_description), leading: const Icon(Icons.download), onTap: exportDatabase, ), ListTile( title: Text( - "reset_sqlite".t(context: context), + context.t.reset_sqlite, style: TextStyle(color: context.colorScheme.error, fontWeight: FontWeight.w500), ), leading: Icon(Icons.settings_backup_restore_rounded, color: context.colorScheme.error), @@ -282,7 +269,7 @@ class _SyncStatsCounts extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle(title: "assets".t(context: context)), + SettingGroupTitle(title: context.t.assets), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), // 1. Wrap in IntrinsicHeight @@ -295,24 +282,16 @@ class _SyncStatsCounts extends ConsumerWidget { spacing: 8.0, children: [ Expanded( - child: EntityCountTile( - label: "local".t(context: context), - count: localAssetCount, - icon: Icons.smartphone, - ), + child: EntityCountTile(label: context.t.local, count: localAssetCount, icon: Icons.smartphone), ), Expanded( - child: EntityCountTile( - label: "remote".t(context: context), - count: remoteAssetCount, - icon: Icons.cloud, - ), + child: EntityCountTile(label: context.t.remote, count: remoteAssetCount, icon: Icons.cloud), ), ], ), ), ), - SettingGroupTitle(title: "albums".t(context: context)), + SettingGroupTitle(title: context.t.albums), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: IntrinsicHeight( @@ -323,24 +302,16 @@ class _SyncStatsCounts extends ConsumerWidget { spacing: 8.0, children: [ Expanded( - child: EntityCountTile( - label: "local".t(context: context), - count: localAlbumCount, - icon: Icons.smartphone, - ), + child: EntityCountTile(label: context.t.local, count: localAlbumCount, icon: Icons.smartphone), ), Expanded( - child: EntityCountTile( - label: "remote".t(context: context), - count: remoteAlbumCount, - icon: Icons.cloud, - ), + child: EntityCountTile(label: context.t.remote, count: remoteAlbumCount, icon: Icons.cloud), ), ], ), ), ), - SettingGroupTitle(title: "other".t(context: context)), + SettingGroupTitle(title: context.t.other), Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: IntrinsicHeight( @@ -351,18 +322,10 @@ class _SyncStatsCounts extends ConsumerWidget { spacing: 8.0, children: [ Expanded( - child: EntityCountTile( - label: "memories".t(context: context), - count: memoryCount, - icon: Icons.calendar_today, - ), + child: EntityCountTile(label: context.t.memories, count: memoryCount, icon: Icons.calendar_today), ), Expanded( - child: EntityCountTile( - label: "hashed_assets".t(context: context), - count: localHashedCount, - icon: Icons.tag, - ), + child: EntityCountTile(label: context.t.hashed_assets, count: localHashedCount, icon: Icons.tag), ), ], ), @@ -371,7 +334,7 @@ class _SyncStatsCounts extends ConsumerWidget { // To be removed once the experimental feature is stable if (CurrentPlatform.isAndroid && appSettingsService.getSetting(AppSettingsEnum.manageLocalMediaAndroid)) ...[ - SettingGroupTitle(title: "trash".t(context: context)), + SettingGroupTitle(title: context.t.trash), Consumer( builder: (context, ref, _) { final counts = ref.watch(trashedAssetsCountProvider); @@ -387,17 +350,13 @@ class _SyncStatsCounts extends ConsumerWidget { children: [ Expanded( child: EntityCountTile( - label: "local".t(context: context), + label: context.t.local, count: c.total, icon: Icons.delete_outline, ), ), Expanded( - child: EntityCountTile( - label: "hashed_assets".t(context: context), - count: c.hashed, - icon: Icons.tag, - ), + child: EntityCountTile(label: context.t.hashed_assets, count: c.hashed, icon: Icons.tag), ), ], ), diff --git a/mobile/lib/widgets/settings/free_up_space_settings.dart b/mobile/lib/widgets/settings/free_up_space_settings.dart index 8d4785d664..5885601171 100644 --- a/mobile/lib/widgets/settings/free_up_space_settings.dart +++ b/mobile/lib/widgets/settings/free_up_space_settings.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; @@ -9,12 +8,13 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/cleanup.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; +import 'package:intl/intl.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; class FreeUpSpaceSettings extends ConsumerStatefulWidget { @@ -236,21 +236,21 @@ class _FreeUpSpaceSettingsState extends ConsumerState { final parts = []; if (state.keepMediaType == AssetKeepType.photosOnly) { - parts.add('all_photos'.t(context: context)); + parts.add(context.t.all_photos); } else if (state.keepMediaType == AssetKeepType.videosOnly) { - parts.add('all_videos'.t(context: context)); + parts.add(context.t.all_videos); } if (state.keepFavorites) { - parts.add('favorites'.t(context: context)); + parts.add(context.t.favorites); } if (state.keepAlbumIds.isNotEmpty) { - parts.add('keep_albums_count'.t(context: context, args: {'count': state.keepAlbumIds.length.toString()})); + parts.add(context.t.keep_albums_count(count: state.keepAlbumIds.length)); } if (parts.isEmpty) { - return 'none'.t(context: context); + return context.t.none; } return parts.join(', '); @@ -274,7 +274,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { borderRadius: const BorderRadius.all(Radius.circular(12)), border: Border.all(color: context.primaryColor.withValues(alpha: 0.25)), ), - child: Text('free_up_space_description'.t(context: context), style: context.textTheme.bodyMedium), + child: Text(context.t.free_up_space_description, style: context.textTheme.bodyMedium), ), ), @@ -307,7 +307,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { color: hasKeepSettings ? context.colorScheme.primary : context.colorScheme.onSurfaceVariant, ), title: Text( - 'keep_on_device'.t(context: context), + context.t.keep_on_device, style: context.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, color: hasKeepSettings ? context.colorScheme.primary : null, @@ -315,8 +315,8 @@ class _FreeUpSpaceSettingsState extends ConsumerState { ), subtitle: Text( hasKeepSettings - ? 'keeping'.t(context: context, args: {'items': getKeepSettingsSummary()}) - : 'keep_on_device_hint'.t(context: context), + ? context.t.keeping(items: getKeepSettingsSummary()) + : context.t.keep_on_device_hint, style: context.textTheme.bodySmall?.copyWith( color: hasKeepSettings ? context.colorScheme.primary : context.colorScheme.onSurfaceVariant, ), @@ -327,12 +327,12 @@ class _FreeUpSpaceSettingsState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('keep_description'.t(context: context), style: subtitleStyle), + Text(context.t.keep_description, style: subtitleStyle), const SizedBox(height: 4), SwitchListTile( contentPadding: EdgeInsets.zero, title: Text( - 'keep_favorites'.t(context: context), + context.t.keep_favorites, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), @@ -352,7 +352,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { ), const SizedBox(height: 16), Text( - 'always_keep'.t(context: context), + context.t.always_keep, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), const SizedBox(height: 4), @@ -362,12 +362,12 @@ class _FreeUpSpaceSettingsState extends ConsumerState { const ButtonSegment(value: AssetKeepType.none, label: Text('—')), ButtonSegment( value: AssetKeepType.photosOnly, - label: Text('photos'.t(context: context)), + label: Text(context.t.photos), icon: const Icon(Icons.photo), ), ButtonSegment( value: AssetKeepType.videosOnly, - label: Text('videos'.t(context: context)), + label: Text(context.t.videos), icon: const Icon(Icons.videocam), ), ], @@ -381,8 +381,8 @@ class _FreeUpSpaceSettingsState extends ConsumerState { const SizedBox(height: 8), Text( state.keepMediaType == AssetKeepType.photosOnly - ? 'always_keep_photos_hint'.t(context: context) - : 'always_keep_videos_hint'.t(context: context), + ? context.t.always_keep_photos_hint + : context.t.always_keep_videos_hint, style: context.textTheme.bodySmall?.copyWith( color: context.colorScheme.onSurfaceVariant, ), @@ -413,7 +413,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { Step( stepStyle: styleForState(step1State), title: Text( - 'select_cutoff_date'.t(context: context), + context.t.select_cutoff_date, style: context.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: step1State == StepState.complete @@ -433,7 +433,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { content: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('cutoff_date_description'.t(context: context), style: subtitleStyle), + Text(context.t.cutoff_date_description, style: subtitleStyle), const SizedBox(height: 16), GridView.count( shrinkWrap: true, @@ -445,39 +445,39 @@ class _FreeUpSpaceSettingsState extends ConsumerState { children: [ _DatePresetCard( value: '30', - unit: 'cutoff_day'.t(context: context, args: {'count': '30'}), + unit: context.t.cutoff_day(count: 30), onTap: () => _setPresetDate(30), isSelected: _isPresetSelected(30), ), _DatePresetCard( value: '60', - unit: 'cutoff_day'.t(context: context, args: {'count': '60'}), + unit: context.t.cutoff_day(count: 60), onTap: () => _setPresetDate(60), isSelected: _isPresetSelected(60), ), _DatePresetCard( value: '90', - unit: 'cutoff_day'.t(context: context, args: {'count': '90'}), + unit: context.t.cutoff_day(count: 90), onTap: () => _setPresetDate(90), isSelected: _isPresetSelected(90), ), _DatePresetCard( value: '1', - unit: 'cutoff_year'.t(context: context, args: {'count': '1'}), + unit: context.t.cutoff_year(count: 1), onTap: () => _setPresetDate(365), isSelected: _isPresetSelected(365), ), _DatePresetCard( value: '2', - unit: 'cutoff_year'.t(context: context, args: {'count': '2'}), + unit: context.t.cutoff_year(count: 2), onTap: () => _setPresetDate(730), isSelected: _isPresetSelected(730), ), _DatePresetCard( value: '3', - unit: 'cutoff_year'.t(context: context, args: {'count': '3'}), + unit: context.t.cutoff_year(count: 3), onTap: () => _setPresetDate(1095), isSelected: _isPresetSelected(1095), ), @@ -487,14 +487,14 @@ class _FreeUpSpaceSettingsState extends ConsumerState { OutlinedButton.icon( onPressed: _selectDate, icon: const Icon(Icons.calendar_today), - label: Text('custom_date'.t(context: context)), + label: Text(context.t.custom_date), style: OutlinedButton.styleFrom(minimumSize: const Size(double.infinity, 48)), ), const SizedBox(height: 16), ElevatedButton.icon( onPressed: hasDate ? _goToScanStep : null, icon: const Icon(Icons.arrow_forward), - label: Text('continue'.t(context: context)), + label: Text(context.t.continue$), style: ElevatedButton.styleFrom(minimumSize: const Size(double.infinity, 48)), ), ], @@ -507,7 +507,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { Step( stepStyle: styleForState(step2State), title: Text( - 'scan'.t(context: context), + context.t.scan, style: context.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: step2State == StepState.complete @@ -520,17 +520,11 @@ class _FreeUpSpaceSettingsState extends ConsumerState { subtitle: _hasScanned ? Text( state.totalBytes > 0 - ? 'cleanup_found_assets_with_size'.t( - context: context, - args: { - 'count': state.assetsToDelete.length.toString(), - 'size': formatBytes(state.totalBytes), - }, + ? context.t.cleanup_found_assets_with_size( + count: state.assetsToDelete.length, + size: formatBytes(state.totalBytes), ) - : 'cleanup_found_assets'.t( - context: context, - args: {'count': state.assetsToDelete.length.toString()}, - ), + : context.t.cleanup_found_assets(count: state.assetsToDelete.length), style: context.textTheme.bodyMedium?.copyWith( color: state.assetsToDelete.isNotEmpty ? context.colorScheme.primary @@ -541,7 +535,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { : null, content: Column( children: [ - Text('cleanup_step3_description'.t(context: context), style: subtitleStyle), + Text(context.t.cleanup_step3_description, style: subtitleStyle), if (CurrentPlatform.isIOS) ...[ const SizedBox(height: 12), Container( @@ -556,7 +550,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { const SizedBox(width: 12), Expanded( child: Text( - 'cleanup_icloud_shared_albums_excluded'.t(context: context), + context.t.cleanup_icloud_shared_albums_excluded, style: context.textTheme.labelLarge, ), ), @@ -577,7 +571,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { : ElevatedButton.icon( onPressed: state.isScanning ? null : _scanAssets, icon: const Icon(Icons.search), - label: Text(_hasScanned ? 'rescan'.t(context: context) : 'scan'.t(context: context)), + label: Text(_hasScanned ? context.t.rescan : context.t.scan), style: ElevatedButton.styleFrom(minimumSize: const Size(double.infinity, 48)), ), if (_hasScanned && state.assetsToDelete.isEmpty) ...[ @@ -593,10 +587,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { const Icon(Icons.info, color: Colors.orange), const SizedBox(width: 12), Expanded( - child: Text( - 'cleanup_no_assets_found'.t(context: context), - style: context.textTheme.bodyMedium, - ), + child: Text(context.t.cleanup_no_assets_found, style: context.textTheme.bodyMedium), ), ], ), @@ -612,7 +603,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { Step( stepStyle: styleForState(step3State, isDestructive: true), title: Text( - 'move_to_device_trash'.t(context: context), + context.t.move_to_device_trash, style: context.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: step3State == StepState.disabled @@ -635,12 +626,9 @@ class _FreeUpSpaceSettingsState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'cleanup_step4_summary'.t( - context: context, - args: { - 'count': state.assetsToDelete.length.toString(), - 'date': DateFormat.yMMMd().format(state.selectedDate!), - }, + context.t.cleanup_step4_summary( + count: state.assetsToDelete.length, + date: DateFormat.yMMMd().format(state.selectedDate!), ), style: context.textTheme.labelLarge?.copyWith(fontSize: 15), ), @@ -652,7 +640,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { OutlinedButton.icon( onPressed: () => _showAssetsPreview(state.assetsToDelete), icon: const Icon(Icons.preview), - label: Text('preview'.t(context: context)), + label: Text(context.t.preview), style: OutlinedButton.styleFrom(minimumSize: const Size(double.infinity, 48)), ), const SizedBox(height: 12), @@ -665,11 +653,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : const Icon(Icons.delete_forever), - label: Text( - state.isDeleting - ? 'cleanup_deleting'.t(context: context) - : 'move_to_device_trash'.t(context: context), - ), + label: Text(state.isDeleting ? context.t.cleanup_deleting : context.t.move_to_device_trash), style: ElevatedButton.styleFrom( backgroundColor: context.colorScheme.error, foregroundColor: context.colorScheme.onError, @@ -701,26 +685,20 @@ class _DeleteConfirmationDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: Text('cleanup_confirm_prompt_title'.t(context: context)), + title: Text(context.t.cleanup_confirm_prompt_title), content: Text( - 'cleanup_confirm_description'.t( - context: context, - args: {'count': assetCount.toString(), 'date': DateFormat.yMMMd().format(cutoffDate)}, - ), + context.t.cleanup_confirm_description(count: assetCount, date: DateFormat.yMMMd().format(cutoffDate)), style: context.textTheme.labelLarge?.copyWith(fontSize: 15), ), actions: [ - TextButton( - onPressed: () => ContextHelper(context).pop(false), - child: Text('cancel'.t(context: context)), - ), + TextButton(onPressed: () => ContextHelper(context).pop(false), child: Text(context.t.cancel)), ElevatedButton( onPressed: () => ContextHelper(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: context.colorScheme.error, foregroundColor: context.colorScheme.onError, ), - child: Text('confirm'.t(context: context)), + child: Text(context.t.confirm), ), ], ); @@ -736,29 +714,24 @@ class _DeleteSuccessDialog extends StatelessWidget { Widget build(BuildContext context) { return AlertDialog( icon: Icon(Icons.check_circle, color: context.colorScheme.primary, size: 48), - title: Text('success'.t(context: context)), + title: Text(context.t.success), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text( - 'cleanup_deleted_assets'.t(context: context, args: {'count': deletedCount.toString()}), + context.t.cleanup_deleted_assets(count: deletedCount), style: context.textTheme.labelLarge?.copyWith(fontSize: 16), textAlign: TextAlign.center, ), const SizedBox(height: 16), Text( - 'cleanup_trash_hint'.t(context: context), + context.t.cleanup_trash_hint, style: context.textTheme.labelLarge?.copyWith(fontSize: 16, color: context.primaryColor), textAlign: TextAlign.center, ), ], ), - actions: [ - ElevatedButton( - onPressed: () => ContextHelper(context).pop(), - child: Text('done'.t(context: context)), - ), - ], + actions: [ElevatedButton(onPressed: () => ContextHelper(context).pop(), child: Text(context.t.done))], ); } } @@ -824,7 +797,7 @@ class _KeepAlbumsSection extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'keep_albums'.t(context: context), + context.t.keep_albums, style: context.textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), @@ -834,13 +807,13 @@ class _KeepAlbumsSection extends ConsumerWidget { child: Padding(padding: EdgeInsets.all(16.0), child: CircularProgressIndicator(strokeWidth: 2)), ), error: (error, stack) => Text( - 'error_loading_albums'.t(context: context), + context.t.error_loading_albums, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.error), ), data: (albums) { if (albums.isEmpty) { return Text( - 'no_albums_found'.t(context: context), + context.t.no_albums_found, style: context.textTheme.bodyMedium?.copyWith( color: context.colorScheme.onSurface.withValues(alpha: 0.6), ), @@ -870,7 +843,7 @@ class _KeepAlbumsSection extends ConsumerWidget { if (albumIds.isNotEmpty) ...[ const SizedBox(height: 8), Text( - 'keep_albums_count'.t(context: context, args: {'count': albumIds.length.toString()}), + context.t.keep_albums_count(count: albumIds.length), style: context.textTheme.bodySmall?.copyWith( color: context.colorScheme.primary, fontWeight: FontWeight.w500, diff --git a/mobile/lib/widgets/settings/language_settings.dart b/mobile/lib/widgets/settings/language_settings.dart index e8478da453..61f2088979 100644 --- a/mobile/lib/widgets/settings/language_settings.dart +++ b/mobile/lib/widgets/settings/language_settings.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @@ -152,7 +152,7 @@ class _LanguageSearchBar extends StatelessWidget { child: SearchField( autofocus: false, contentPadding: const EdgeInsets.all(12), - hintText: 'language_search_hint'.t(context: context), + hintText: context.t.language_search_hint, prefixIcon: const Icon(Icons.search_rounded), suffixIcon: controller.text.isNotEmpty ? IconButton(icon: const Icon(Icons.clear_rounded), onPressed: onClear) @@ -179,12 +179,12 @@ class _LanguageNotFound extends StatelessWidget { Icon(Icons.search_off_rounded, size: 64, color: context.colorScheme.onSurface.withValues(alpha: 0.4)), const SizedBox(height: 8), Text( - 'language_no_results_title'.t(context: context), + context.t.language_no_results_title, style: context.textTheme.titleMedium?.copyWith(color: context.colorScheme.onSurface), ), const SizedBox(height: 4), Text( - 'language_no_results_subtitle'.t(context: context), + context.t.language_no_results_subtitle, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurface.withValues(alpha: 0.8)), ), ], @@ -214,7 +214,7 @@ class _LanguageApplyButton extends StatelessWidget { child: isLoading ? const SizedBox.square(dimension: 24, child: CircularProgressIndicator(strokeWidth: 2)) : Text( - 'setting_languages_apply'.t(context: context), + context.t.setting_languages_apply, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0), ), ), diff --git a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart index 162f1252a8..349b32287b 100644 --- a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart +++ b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart'; @@ -78,11 +78,11 @@ class EndpointInputState extends ConsumerState { try { if (url == null || url.isEmpty || !Uri.parse(url).isAbsolute) { isInputValid = false; - return 'validate_endpoint_error'.tr(); + return context.t.validate_endpoint_error; } } catch (_) { isInputValid = false; - return 'validate_endpoint_error'.tr(); + return context.t.validate_endpoint_error; } isInputValid = true; diff --git a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart index 81ffb4b19b..63b094b440 100644 --- a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart @@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; @@ -103,7 +103,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 24), - child: Text("external_network_sheet_info".t(context: context), style: context.textTheme.bodyMedium), + child: Text(context.t.external_network_sheet_info, style: context.textTheme.bodyMedium), ), const SizedBox(height: 4), Divider(color: context.colorScheme.surfaceContainerHighest), @@ -135,7 +135,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { height: 48, child: OutlinedButton.icon( icon: const Icon(Icons.add), - label: Text('add_endpoint'.t(context: context)), + label: Text(context.t.add_endpoint), onPressed: enabled ? () { entries.value = [ diff --git a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart index 8b787bcaea..3c4fa73543 100644 --- a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart @@ -1,11 +1,10 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/network.provider.dart'; import 'package:immich_ui/immich_ui.dart'; @@ -34,9 +33,12 @@ class LocalNetworkPreference extends HookConsumerWidget { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: Text('cancel'.tr().toUpperCase(), style: const TextStyle(color: Colors.red)), + child: Text(context.t.cancel.toUpperCase(), style: const TextStyle(color: Colors.red)), + ), + TextButton( + onPressed: () => Navigator.pop(context, controller.text), + child: Text(context.t.save.toUpperCase()), ), - TextButton(onPressed: () => Navigator.pop(context, controller.text), child: Text('save'.tr().toUpperCase())), ], ), ); @@ -73,7 +75,12 @@ class LocalNetworkPreference extends HookConsumerWidget { } Future handleEditWifiName() async { - final wifiName = await _showEditDialog(context, "wifi_name".tr(), "your_wifi_name".tr(), wifiNameText.value); + final wifiName = await _showEditDialog( + context, + context.t.wifi_name, + context.t.your_wifi_name, + wifiNameText.value, + ); if (wifiName != null) { await saveWifiName(wifiName); @@ -83,7 +90,7 @@ class LocalNetworkPreference extends HookConsumerWidget { Future handleEditServerEndpoint() async { final localEndpoint = await _showEditDialog( context, - "server_endpoint".tr(), + context.t.server_endpoint, "http://local-ip:2283", localEndpointText.value, isUrlField: true, @@ -104,7 +111,7 @@ class LocalNetworkPreference extends HookConsumerWidget { context.showSnackBar( SnackBar( content: Text( - "get_wifiname_error".tr(), + context.t.get_wifiname_error, style: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w500, color: context.colorScheme.onSecondary, @@ -149,7 +156,7 @@ class LocalNetworkPreference extends HookConsumerWidget { children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 24), - child: Text("local_network_sheet_info".tr(), style: context.textTheme.bodyMedium), + child: Text(context.t.local_network_sheet_info, style: context.textTheme.bodyMedium), ), const SizedBox(height: 4), Divider(color: context.colorScheme.surfaceContainerHighest), @@ -157,9 +164,9 @@ class LocalNetworkPreference extends HookConsumerWidget { enabled: enabled, contentPadding: const EdgeInsets.only(left: 24, right: 8), leading: const Icon(Icons.wifi_rounded), - title: Text("wifi_name".tr()), + title: Text(context.t.wifi_name), subtitle: wifiNameText.value.isEmpty - ? Text("enter_wifi_name".tr()) + ? Text(context.t.enter_wifi_name) : Text( wifiNameText.value, style: context.textTheme.labelLarge?.copyWith( @@ -177,7 +184,7 @@ class LocalNetworkPreference extends HookConsumerWidget { enabled: enabled, contentPadding: const EdgeInsets.only(left: 24, right: 8), leading: const Icon(Icons.lan_rounded), - title: Text("server_endpoint".t(context: context)), + title: Text(context.t.server_endpoint), subtitle: localEndpointText.value.isEmpty ? const Text("http://local-ip:2283") : Text( @@ -199,7 +206,7 @@ class LocalNetworkPreference extends HookConsumerWidget { height: 48, child: OutlinedButton.icon( icon: const Icon(Icons.wifi_find_rounded), - label: Text('use_current_connection'.t(context: context)), + label: Text(context.t.use_current_connection), onPressed: enabled ? autofillCurrentNetwork : null, ), ), diff --git a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart index dad827230a..581bae4324 100644 --- a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart +++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart @@ -1,11 +1,10 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/network.provider.dart'; @@ -39,8 +38,8 @@ class NetworkingSettings extends HookConsumerWidget { context: context, builder: (context) { return AlertDialog( - title: Text("location_permission".tr()), - content: Text("location_permission_content".tr()), + title: Text(context.t.location_permission), + content: Text(context.t.location_permission_content), actions: [ TextButton( onPressed: () async { @@ -52,7 +51,7 @@ class NetworkingSettings extends HookConsumerWidget { Navigator.pop(context, isGrant); }, - child: Text("grant_permission".tr()), + child: Text(context.t.grant_permission), ), ], ); @@ -69,8 +68,8 @@ class NetworkingSettings extends HookConsumerWidget { context: context, builder: (context) { return AlertDialog( - title: Text("background_location_permission".tr()), - content: Text("background_location_permission_content".tr()), + title: Text(context.t.background_location_permission), + content: Text(context.t.background_location_permission_content), actions: [ TextButton( onPressed: () async { @@ -82,7 +81,7 @@ class NetworkingSettings extends HookConsumerWidget { Navigator.pop(context, isGrant); }, - child: Text("grant_permission".tr()), + child: Text(context.t.grant_permission), ), ], ); @@ -107,7 +106,7 @@ class NetworkingSettings extends HookConsumerWidget { children: [ const SizedBox(height: 8), SettingGroupTitle( - title: "current_server_address".t(context: context), + title: context.t.current_server_address, icon: (currentEndpoint?.startsWith('https') ?? false) ? Icons.https_outlined : Icons.http_outlined, ), Padding( @@ -136,20 +135,14 @@ class NetworkingSettings extends HookConsumerWidget { SettingsSwitchListTile( enabled: true, valueNotifier: featureEnabled, - title: "automatic_endpoint_switching_title".tr(), - subtitle: "automatic_endpoint_switching_subtitle".tr(), + title: context.t.automatic_endpoint_switching_title, + subtitle: context.t.automatic_endpoint_switching_subtitle, ), const SizedBox(height: 8), - SettingGroupTitle( - title: "local_network".t(context: context), - icon: Icons.home_outlined, - ), + SettingGroupTitle(title: context.t.local_network, icon: Icons.home_outlined), LocalNetworkPreference(enabled: featureEnabled.value), const SizedBox(height: 16), - SettingGroupTitle( - title: "external_network".t(context: context), - icon: Icons.dns_outlined, - ), + SettingGroupTitle(title: context.t.external_network, icon: Icons.dns_outlined), ExternalNetworkPreference(enabled: featureEnabled.value), ], ); diff --git a/mobile/lib/widgets/settings/notification_setting.dart b/mobile/lib/widgets/settings/notification_setting.dart index 0bab4e051a..c3d175999d 100644 --- a/mobile/lib/widgets/settings/notification_setting.dart +++ b/mobile/lib/widgets/settings/notification_setting.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/permission.provider.dart'; import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; @@ -29,10 +29,10 @@ class NotificationSetting extends HookConsumerWidget { showDialog( context: context, builder: (ctx) => AlertDialog( - content: const Text('notification_permission_dialog_content').tr(), + content: Text(ctx.t.notification_permission_dialog_content), actions: [ - TextButton(child: const Text('cancel').tr(), onPressed: () => ctx.pop()), - TextButton(onPressed: () => openAppNotificationSettings(ctx), child: const Text('settings').tr()), + TextButton(child: Text(ctx.t.cancel), onPressed: () => ctx.pop()), + TextButton(onPressed: () => openAppNotificationSettings(ctx), child: Text(ctx.t.settings)), ], ), ), @@ -43,9 +43,9 @@ class NotificationSetting extends HookConsumerWidget { if (!hasPermission) SettingsButtonListTile( icon: Icons.notifications_outlined, - title: 'notification_permission_list_tile_title'.tr(), - subtileText: 'notification_permission_list_tile_content'.tr(), - buttonText: 'notification_permission_list_tile_enable_button'.tr(), + title: context.t.notification_permission_list_tile_title, + subtileText: context.t.notification_permission_list_tile_content, + buttonText: context.t.notification_permission_list_tile_enable_button, onButtonTap: () => ref.read(notificationPermissionProvider.notifier).requestNotificationPermission().then((permission) { if (permission == PermissionStatus.permanentlyDenied) { @@ -56,9 +56,9 @@ class NotificationSetting extends HookConsumerWidget { else SettingsButtonListTile( icon: Icons.notifications_active_outlined, - title: 'notification_enabled_list_tile_title'.tr(), - subtileText: 'notification_enabled_list_tile_content'.tr(), - buttonText: 'notification_enabled_list_tile_open_button'.tr(), + title: context.t.notification_enabled_list_tile_title, + subtileText: context.t.notification_enabled_list_tile_content, + buttonText: context.t.notification_enabled_list_tile_open_button, onButtonTap: () => openAppSettings(), ), ]; diff --git a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart index c5003a9ced..9aaae683ab 100644 --- a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; @@ -21,13 +21,10 @@ class HapticSetting extends HookWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle( - title: "haptic_feedback_title".t(context: context), - icon: Icons.vibration_outlined, - ), + SettingGroupTitle(title: context.t.haptic_feedback_title, icon: Icons.vibration_outlined), SettingsSwitchListTile( valueNotifier: isHapticFeedbackEnabled, - title: 'enabled'.t(context: context), + title: context.t.enabled, onChanged: onHapticFeedbackChange, ), ], diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index 7964ec3861..37235a182a 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/theme/color_scheme.dart'; @@ -97,7 +97,7 @@ class PrimaryColorSetting extends HookConsumerWidget { children: [ Align( alignment: Alignment.center, - child: Text("theme_setting_primary_color_title".tr(), style: context.textTheme.titleLarge), + child: Text(context.t.theme_setting_primary_color_title, style: context.textTheme.titleLarge), ), if (DynamicTheme.isAvailable) Container( @@ -110,7 +110,7 @@ class PrimaryColorSetting extends HookConsumerWidget { tileColor: context.colorScheme.surfaceContainerHigh, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15))), title: Text( - 'theme_setting_system_primary_color_title'.tr(), + context.t.theme_setting_system_primary_color_title, style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), value: themeConfig.dynamicTheme, @@ -157,11 +157,11 @@ class PrimaryColorSetting extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "theme_setting_primary_color_title".tr(), + context.t.theme_setting_primary_color_title, style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), ), Text( - "theme_setting_primary_color_subtitle".tr(), + context.t.theme_setting_primary_color_subtitle, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), ], diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index 8281b2262a..cf1a22c45f 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; @@ -52,26 +52,23 @@ class ThemeSetting extends HookConsumerWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingGroupTitle( - title: "theme".t(context: context), - icon: Icons.color_lens_outlined, - ), + SettingGroupTitle(title: context.t.theme, icon: Icons.color_lens_outlined), SettingsSwitchListTile( valueNotifier: isSystemTheme, - title: 'theme_setting_system_theme_switch'.t(context: context), + title: context.t.theme_setting_system_theme_switch, onChanged: onSystemThemeChange, ), if (currentTheme.value != ThemeMode.system) SettingsSwitchListTile( valueNotifier: isDarkTheme, - title: 'map_settings_dark_mode'.t(context: context), + title: context.t.map_settings_dark_mode, onChanged: onThemeChange, ), const PrimaryColorSetting(), SettingsSwitchListTile( valueNotifier: colorfulInterface, - title: "theme_setting_colorful_interface_title".t(context: context), - subtitle: 'theme_setting_colorful_interface_subtitle'.t(context: context), + title: context.t.theme_setting_colorful_interface_title, + subtitle: context.t.theme_setting_colorful_interface_subtitle, onChanged: onSurfaceColorSettingChange, ), ], diff --git a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart index 77ad7ee179..f37aa04b33 100644 --- a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart +++ b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart @@ -1,10 +1,10 @@ import 'dart:async'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/platform/network_api.g.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:logging/logging.dart'; @@ -44,12 +44,15 @@ class _SslClientCertSettingsState extends State { contentPadding: const EdgeInsets.symmetric(horizontal: 20), horizontalTitleGap: 20, isThreeLine: true, - title: Text("client_cert_title".tr(), style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500)), + title: Text( + context.t.client_cert_title, + style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500), + ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "client_cert_subtitle".tr(), + context.t.client_cert_subtitle, style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), const SizedBox(height: 6), @@ -58,8 +61,8 @@ class _SslClientCertSettingsState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ - ElevatedButton(onPressed: importCert, child: Text("client_cert_import".tr())), - ElevatedButton(onPressed: !isCertExist ? null : removeCert, child: Text("remove".tr())), + ElevatedButton(onPressed: importCert, child: Text(context.t.client_cert_import)), + ElevatedButton(onPressed: !isCertExist ? null : removeCert, child: Text(context.t.remove)), ], ), ], @@ -79,20 +82,20 @@ class _SslClientCertSettingsState extends State { Future importCert() async { try { final styling = ClientCertPrompt( - title: "client_cert_password_title".tr(), - message: "client_cert_password_message".tr(), - cancel: "cancel".tr(), - confirm: "confirm".tr(), + title: context.t.client_cert_password_title, + message: context.t.client_cert_password_message, + cancel: context.t.cancel, + confirm: context.t.confirm, ); await networkApi.selectCertificate(styling); setState(() => isCertExist = true); - showMessage("client_cert_import_success_msg".tr()); + showMessage(StaticTranslations.instance.client_cert_import_success_msg); } catch (e) { if (_isCancellation(e)) { return; } _log.severe("Error importing client cert", e); - showMessage("client_cert_invalid_msg".tr()); + showMessage(StaticTranslations.instance.client_cert_invalid_msg); } } @@ -100,13 +103,13 @@ class _SslClientCertSettingsState extends State { try { await networkApi.removeCertificate(); setState(() => isCertExist = false); - showMessage("client_cert_remove_msg".tr()); + showMessage(StaticTranslations.instance.client_cert_remove_msg); } catch (e) { if (_isCancellation(e)) { return; } _log.severe("Error removing client cert", e); - showMessage("client_cert_invalid_msg".tr()); + showMessage(StaticTranslations.instance.client_cert_invalid_msg); } } diff --git a/mobile/lib/widgets/shared_link/shared_link_item.dart b/mobile/lib/widgets/shared_link/shared_link_item.dart index d98925bdf8..2471c19cea 100644 --- a/mobile/lib/widgets/shared_link/shared_link_item.dart +++ b/mobile/lib/widgets/shared_link/shared_link_item.dart @@ -173,8 +173,8 @@ class SharedLinkItem extends ConsumerWidget { final confirmed = await showDialog( context: context, builder: (BuildContext context) => ConfirmDialog( - title: "delete_shared_link_dialog_title", - content: "confirm_delete_shared_link", + title: context.t.delete_shared_link_dialog_title, + content: context.t.confirm_delete_shared_link, onOk: () {}, ), ); diff --git a/mobile/mise.lock b/mobile/mise.lock index 8f2f9572fb..cd3ac25848 100644 --- a/mobile/mise.lock +++ b/mobile/mise.lock @@ -1,30 +1,30 @@ # @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html [[tools."aqua:flutter/flutter"]] -version = "3.44.8" +version = "3.44.9" backend = "aqua:flutter/flutter" [tools."aqua:flutter/flutter"."platforms.linux-arm64"] -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.9-stable.tar.xz" [tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"] -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.9-stable.tar.xz" [tools."aqua:flutter/flutter"."platforms.linux-x64"] -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.9-stable.tar.xz" [tools."aqua:flutter/flutter"."platforms.linux-x64-musl"] -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.8-stable.tar.xz" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.9-stable.tar.xz" [tools."aqua:flutter/flutter"."platforms.macos-arm64"] -checksum = "blake3:79c780876c64f5a66c015845f544bdb20275f7f3bf819e6c831f23df3e96b8db" -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.8-stable.zip" +checksum = "blake3:e5c14d6b5e917a8578327d2564ab78ece83cd685c74c29572ddaf7655f42205d" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.9-stable.zip" [tools."aqua:flutter/flutter"."platforms.macos-x64"] -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.8-stable.zip" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.9-stable.zip" [tools."aqua:flutter/flutter"."platforms.windows-x64"] -url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.8-stable.zip" +url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.9-stable.zip" [[tools."github:CQLabs/homebrew-dcm"]] version = "1.37.0" diff --git a/mobile/mise.toml b/mobile/mise.toml index 7caf6dfbd5..d8ddc8ab21 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -1,5 +1,5 @@ [tools] -"aqua:flutter/flutter" = "3.44.8" +"aqua:flutter/flutter" = "3.44.9" java = "21.0.2" [tools."github:CQLabs/homebrew-dcm"] @@ -29,7 +29,7 @@ sources = [ ] outputs = { auto = true } run = [ - "dart run build_runner build", + "dart run build_runner build", "dart format lib/routing/router.gr.dart", ] depends = ["//:open-api-dart"] @@ -99,7 +99,11 @@ run = "dart run drift_dev make-migrations" alias = "install" description = "Install flutter dependencies" depends = ["//:open-api-dart"] -run = "flutter pub get" +run = [ + # This receives passed `--enforce-lockfile` args + "flutter pub get", + { task = "install:ios" }, +] [tasks."install:ci"] description = "Install flutter dependencies in CI" @@ -107,6 +111,18 @@ run = [ { task = "install", args = ["--enforce-lockfile"] }, ] +[tasks.checkout] +description = "Bring a checkout up to date after switching branches" +run = [ + { task = "install" }, + { task = "checkout:ios" }, + { task = "codegen" }, +] + +[tasks."checkout:ios"] +description = "Bring the iOS project up to date after switching branches" +run = "bash ./scripts/checkout_ios.sh" + [tasks.start] alias = "start" description = "Start flutter app" @@ -132,6 +148,19 @@ run = [ ] wait_for = ["//:i18n:format-fix"] +[tasks."install:ios"] +description = "Install CocoaPods dependencies" +hide = true +sources = ["ios/Podfile", "ios/Podfile.lock", ".flutter-plugins-dependencies"] +outputs = ["ios/Pods/Manifest.lock"] +run = ''' +echo "Running pod install" +if [ "$(uname)" != "Darwin" ]; then + exit 0 +fi +cd ios && pod install +''' + [tasks."analyze:dart"] description = "Run Dart analysis" hide = true diff --git a/mobile/pigeon/remote_image_api.dart b/mobile/pigeon/remote_image_api.dart index 7f0135acb8..277b7016fd 100644 --- a/mobile/pigeon/remote_image_api.dart +++ b/mobile/pigeon/remote_image_api.dart @@ -13,8 +13,15 @@ import 'package:pigeon/pigeon.dart'; ) @HostApi() abstract class RemoteImageApi { + /// Width and height are the physical decode size, or null for the source size. @async - Map? requestImage(String url, {required int requestId, required bool preferEncoded}); + Map? requestImage( + String url, { + required int requestId, + required bool preferEncoded, + int? width, + int? height, + }); void cancelRequest(int requestId); diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index f02df0b622..ac19481079 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -2030,4 +2030,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.12.0 <4.0.0" - flutter: "3.44.8" + flutter: "3.44.9" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index c2848e6dd8..32e67de4f6 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -6,7 +6,7 @@ version: 3.1.0+3057 environment: sdk: '>=3.12.0 <4.0.0' - flutter: 3.44.8 + flutter: 3.44.9 dependencies: async: ^2.13.1 diff --git a/mobile/scripts/checkout_ios.sh b/mobile/scripts/checkout_ios.sh new file mode 100644 index 0000000000..74706e0d23 --- /dev/null +++ b/mobile/scripts/checkout_ios.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Reconciles the iOS project with the current Flutter setup +# +# `flutter build ios --config-only` is relatively expensive, so we attempt to avoid running it when possible + +set -euo pipefail + +stamp="build/ios_checkout.stamp" + +# All tracked files that can invalidate the Xcode + Flutter build state +inputs_digest() { + git ls-files -z ios pubspec.yaml pubspec.lock mise.toml mise.lock \ + | xargs -0 shasum \ + | shasum \ + | cut -d ' ' -f 1 +} + +# Echoes why the iOS project needs reconciling, or returns 1 if it does not. +# The first three cases can happen with no tracked file having changed at all, +# so comparing the digest alone is not enough. +staleness() { + local flutter_root + flutter_root="$(sed -n 's/^FLUTTER_ROOT=//p' ios/Flutter/Generated.xcconfig 2>/dev/null || true)" + + if [[ -z $flutter_root ]]; then + echo "misconfigured FLUTTER_ROOT" + elif [[ ! -d $flutter_root ]]; then + echo "the Flutter SDK has changed" + elif ! cmp -s ios/Podfile.lock ios/Pods/Manifest.lock; then + # This is also done by Xcode's "Check Pods Manifest.lock" build phase + echo "Cocoapods out of sync" + elif [[ ! -f $stamp ]]; then + echo "first checkout" + elif [[ $(<"$stamp") != "$(inputs_digest)" ]]; then + echo "tracked build state has changed" + else + return 1 + fi +} + +[[ $(uname) == Darwin ]] || exit 0 + +if ! reason="$(staleness)"; then + exit 0 +fi + +echo "Reconciling iOS project: $reason" +flutter build ios --config-only --debug + +# Save tracked files manifest +mkdir -p "$(dirname "$stamp")" +inputs_digest > "$stamp" diff --git a/mobile/test/infrastructure/loaders/remote_image_request_test.dart b/mobile/test/infrastructure/loaders/remote_image_request_test.dart new file mode 100644 index 0000000000..97bdf3e2bf --- /dev/null +++ b/mobile/test/infrastructure/loaders/remote_image_request_test.dart @@ -0,0 +1,117 @@ +import 'dart:ffi'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:ffi/ffi.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/infrastructure/loaders/image_request.dart'; +import 'package:immich_mobile/platform/remote_image_api.g.dart'; +import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const channel = BasicMessageChannel( + 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage', + RemoteImageApi.pigeonChannelCodec, + ); + late List args; + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler(channel, ( + message, + ) async { + args = message! as List; + return [null]; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler(channel, null); + }); + + Future loadEncoded(String path, ui.Size decodeSize) async { + final bytes = await File(path).readAsBytes(); + final pointer = malloc(bytes.length)..asTypedList(bytes.length).setAll(0, bytes); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler(channel, ( + message, + ) async { + return [ + {'pointer': pointer.address, 'length': bytes.length}, + ]; + }); + final request = RemoteImageRequest(uri: 'https://example.test/fallback', decodeSize: decodeSize); + + return (await request.load((_, {getTargetSize}) => throw UnimplementedError()))!.image; + } + + test('passes the requested decode size to the platform', () async { + final request = RemoteImageRequest(uri: 'https://example.test/thumbnail', decodeSize: const ui.Size(319.1, 179.1)); + + await request.load((_, {getTargetSize}) => throw UnimplementedError()); + + expect(args[0], 'https://example.test/thumbnail'); + expect(args[2], isFalse); + expect(args[3], 320); + expect(args[4], 180); + }); + + test('leaves requests without a size unbounded', () async { + final request = RemoteImageRequest(uri: 'https://example.test/thumbnail'); + + await request.load((_, {getTargetSize}) => throw UnimplementedError()); + + expect(args[3], isNull); + expect(args[4], isNull); + }); + + test('leaves encoded animation requests unbounded', () async { + final request = RemoteImageRequest(uri: 'https://example.test/animation', decodeSize: const ui.Size(320, 180)); + + await request.loadCodec(); + + expect(args[2], isTrue); + expect(args[3], isNull); + expect(args[4], isNull); + }); + + test('keeps portrait cover quality in a wide tile', () async { + final image = await loadEncoded('assets/feature_message/ocr.webp', const ui.Size(963, 642)); + + expect(image.width, 963); + expect(image.height, 1453); + image.dispose(); + }); + + test('preserves cover quality for extreme aspect ratios', () async { + final image = await loadEncoded('assets/immich-logo-inline-light.png', const ui.Size.square(320)); + + expect(image.width, 1311); + expect(image.height, 320); + image.dispose(); + }); + + test('does not upscale encoded fallback', () async { + final image = await loadEncoded('assets/feature_message/ocr.webp', const ui.Size.square(2000)); + + expect(image.width, 1206); + expect(image.height, 1819); + image.dispose(); + }); + + test('uses the decode size in the provider cache key', () { + final small = RemoteImageProvider(url: 'https://example.test/thumbnail', decodeSize: const ui.Size.square(160)); + final large = RemoteImageProvider(url: 'https://example.test/thumbnail', decodeSize: const ui.Size.square(320)); + + expect(small, isNot(large)); + }); + + test('shares the cache key when no decode size is set', () { + final first = RemoteImageProvider(url: 'https://example.test/thumbnail'); + final second = RemoteImageProvider(url: 'https://example.test/thumbnail'); + + expect(first, second); + expect(first.hashCode, second.hashCode); + }); +} diff --git a/mobile/test/presentation/widgets/timeline/timeline_scroll_to_top_test.dart b/mobile/test/presentation/widgets/timeline/timeline_scroll_to_top_test.dart index 28644d6702..f72af61a0a 100644 --- a/mobile/test/presentation/widgets/timeline/timeline_scroll_to_top_test.dart +++ b/mobile/test/presentation/widgets/timeline/timeline_scroll_to_top_test.dart @@ -13,6 +13,7 @@ import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/routing/app_navigation_observer.dart'; import '../../../fixtures/asset.stub.dart'; @@ -62,7 +63,9 @@ void main() { timelineServiceProvider.overrideWithValue(service), appConfigProvider.overrideWithValue(const AppConfig()), ], - child: MaterialApp.router(routerConfig: router.config()), + child: MaterialApp.router( + routerConfig: router.config(navigatorObservers: () => [TransitioningRouteObserver()]), + ), ), ); // Segment stream resolves @@ -96,9 +99,14 @@ void main() { await tester.pumpAndSettle(); expect(position.pixels, initialScrolledPosition, reason: 'ignored while behind a pushed route'); - // Once the timeline is visible, taps on the status bar should scroll to the top - await router.maybePop(); + // The tap is async, so it can arrive while a pushed route is transitioning to popped + unawaited(router.maybePop()); + await tester.pump(); + await tapStatusBar(); await tester.pumpAndSettle(); + expect(position.pixels, initialScrolledPosition, reason: 'ignored while the pushed route pops'); + + // Once the timeline is visible, taps on the status bar should scroll to the top await tapStatusBar(); await tester.pumpAndSettle(); expect(position.pixels, 0, reason: 'scrolls the foreground timeline'); diff --git a/mobile/test/providers/asset_viewer/asset_viewer_provider_test.dart b/mobile/test/providers/asset_viewer/asset_viewer_provider_test.dart index 27033a5774..454923affa 100644 --- a/mobile/test/providers/asset_viewer/asset_viewer_provider_test.dart +++ b/mobile/test/providers/asset_viewer/asset_viewer_provider_test.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -107,5 +108,20 @@ void main() { await pumpEventQueue(); expect(container.read(assetViewerProvider).currentAsset, updatedTwo); }); + + test('keeps the tapped tile size while swiping', () { + final first = RemoteAssetFactory.create(); + final second = RemoteAssetFactory.create(); + final notifier = container.read(assetViewerProvider.notifier); + + notifier.setAsset(first, thumbnailSize: const Size.square(642)); + expect(container.read(assetViewerProvider).thumbnailSize, const Size.square(642)); + + notifier.setAsset(second); + expect(container.read(assetViewerProvider).thumbnailSize, const Size.square(642)); + + notifier.reset(); + expect(container.read(assetViewerProvider).thumbnailSize, isNull); + }); }); } diff --git a/mobile/test/unit/presentation/actions/delete_action_test.dart b/mobile/test/unit/presentation/actions/delete_action_test.dart index 29d81a5c2c..4c1bf9306c 100644 --- a/mobile/test/unit/presentation/actions/delete_action_test.dart +++ b/mobile/test/unit/presentation/actions/delete_action_test.dart @@ -170,6 +170,17 @@ void main() { verifyNever(() => assetService.trash(any())); verifyNever(() => assetService.delete(any())); }); + + testWidgets('is labelled trash', (tester) async { + await tester.pumpTestWidget( + context, + const ActionButton(action: DeleteAction(source: .timeline)), + overrides: context.selected({LocalAssetFactory.create()}), + ); + + expect(find.text(StaticTranslations.instance.trash), findsOneWidget); + expect(find.text(StaticTranslations.instance.delete), findsNothing); + }); }); group('prompt handling', () { diff --git a/open-api/patch/pubspec_immich_mobile.yaml.patch b/open-api/patch/pubspec_immich_mobile.yaml.patch index ad3bbc2edd..be210f864f 100644 --- a/open-api/patch/pubspec_immich_mobile.yaml.patch +++ b/open-api/patch/pubspec_immich_mobile.yaml.patch @@ -6,4 +6,4 @@ -dev_dependencies: - test: '>=1.21.6 <1.22.0' + immich_mobile: -+ path: ../ ++ path: ../../ diff --git a/package.json b/package.json index 2464012cee..75222c8220 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "format": "prettier --cache --check i18n/", "format:fix": "prettier --cache --write --list-different i18n" }, - "packageManager": "pnpm@11.17.0", + "packageManager": "pnpm@11.20.0", "engines": { "pnpm": ">=10.0.0" }, diff --git a/packages/cli/Dockerfile b/packages/cli/Dockerfile index 716522ecd7..c1c320fc11 100644 --- a/packages/cli/Dockerfile +++ b/packages/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.18.0-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573 AS core +FROM node:24.19.0-trixie-slim@sha256:0711b541c1c33a8a530ac4f0d391baa9a15b3d804695b1b24a47daa5fb60e74d AS core WORKDIR /usr/src/app COPY package* pnpm* .pnpmfile.cjs ./ diff --git a/packages/cli/eslint.config.mjs b/packages/cli/eslint.config.mjs index bcc74c654d..cd32bf6023 100644 --- a/packages/cli/eslint.config.mjs +++ b/packages/cli/eslint.config.mjs @@ -2,13 +2,8 @@ import js from '@eslint/js'; import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; import eslintPluginUnicorn from 'eslint-plugin-unicorn'; import globals from 'globals'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import typescriptEslint from 'typescript-eslint'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - export default typescriptEslint.config([ eslintPluginUnicorn.configs.recommended, eslintPluginPrettierRecommended, @@ -29,7 +24,7 @@ export default typescriptEslint.config([ parserOptions: { project: 'tsconfig.json', - tsconfigRootDir: __dirname, + tsconfigRootDir: import.meta.dirname, }, }, diff --git a/packages/e2e-auth-server/Dockerfile b/packages/e2e-auth-server/Dockerfile index bfc939c084..087fa45d23 100644 --- a/packages/e2e-auth-server/Dockerfile +++ b/packages/e2e-auth-server/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.18.0-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573 +FROM node:24.19.0-trixie-slim@sha256:0711b541c1c33a8a530ac4f0d391baa9a15b3d804695b1b24a47daa5fb60e74d WORKDIR /usr/src/app COPY package* pnpm* .pnpmfile.cjs ./ COPY ./packages ./packages/ diff --git a/packages/e2e-auth-server/package.json b/packages/e2e-auth-server/package.json index b872dfe36d..eae4fd3e04 100644 --- a/packages/e2e-auth-server/package.json +++ b/packages/e2e-auth-server/package.json @@ -13,5 +13,5 @@ "oidc-provider": "^9.0.0", "tsx": "^4.20.6" }, - "packageManager": "pnpm@11.17.0" + "packageManager": "pnpm@11.20.0" } diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index 8afa00cfb4..4ae3ca76a7 100644 --- a/packages/plugin-sdk/package.json +++ b/packages/plugin-sdk/package.json @@ -27,7 +27,7 @@ "keywords": [], "author": "", "license": "GNU Affero General Public License version 3", - "packageManager": "pnpm@11.17.0", + "packageManager": "pnpm@11.20.0", "devDependencies": { "@extism/js-pdk": "^1.1.1", "@immich/sdk": "workspace:*", diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 66327e6150..ca08f10759 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -32,5 +32,5 @@ "vite": "^8.0.16", "vitest": "^4.1.8" }, - "packageManager": "pnpm@11.17.0" + "packageManager": "pnpm@11.20.0" } diff --git a/packages/scripts/src/cli.ts b/packages/scripts/src/cli.ts index c89a0f142c..95e50e9770 100644 --- a/packages/scripts/src/cli.ts +++ b/packages/scripts/src/cli.ts @@ -23,32 +23,25 @@ export const cli = (argv: string[]) => { .choices(RELEASE_TYPES) .makeOptionMandatory(), ) - .addOption( - new Option('-m, --mobile ', 'pump mobile build number') - .choices(['true', 'false']) - .default('false'), - ) - .action( - ({ type, mobile }: { type: ReleaseType; mobile: 'true' | 'false' }) => { - try { - console.log(handleRelease({ type, mobile: mobile === 'true' })); - } catch (error) { - if (error instanceof ReleaseInputError) { - console.log(program.usage()); - process.exit(1); - } - - if (error instanceof ReleaseError) { - console.log( - `Invalid pump: ${type}. Pumping from ${error.version} to ${error.newVersion} is not allowed.`, - ); - process.exit(1); - } - - throw error; + .action(({ type }: { type: ReleaseType }) => { + try { + console.log(handleRelease({ type })); + } catch (error) { + if (error instanceof ReleaseInputError) { + console.log(program.usage()); + process.exit(1); } - }, - ); + + if (error instanceof ReleaseError) { + console.log( + `Invalid pump: ${type}. Pumping from ${error.version} to ${error.newVersion} is not allowed.`, + ); + process.exit(1); + } + + throw error; + } + }); return program.parse(argv); }; diff --git a/packages/scripts/src/commands/release.spec.ts b/packages/scripts/src/commands/release.spec.ts index de769ebdc6..dc3f82517d 100644 --- a/packages/scripts/src/commands/release.spec.ts +++ b/packages/scripts/src/commands/release.spec.ts @@ -1,17 +1,9 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - resolveArchivedVersions, - ArchivedVersion, - getNewVersion, - ReleaseError, -} from './release'; +import semver from 'semver'; +import { describe, expect, it } from 'vitest'; +import { getMobileBuild, getNewVersion, ReleaseError } from './release'; -const archived = (label: string): ArchivedVersion => ({ - label, - url: `https://docs.${label}.archive.immich.app`, -}); - -const labels = (versions: ArchivedVersion[]) => versions.map((v) => v.label); +const mobileBuild = (version: string) => + getMobileBuild(semver.parse(version) as semver.SemVer); describe(getNewVersion.name, () => { describe('transitions', () => { @@ -77,63 +69,55 @@ describe(getNewVersion.name, () => { }); }); -describe(resolveArchivedVersions.name, () => { - beforeEach(() => { - // silence the "Removed ..." progress logging - vi.spyOn(console, 'log').mockImplementation(() => {}); +describe(getMobileBuild.name, () => { + it('should encode the version', () => { + expect(mobileBuild('3.1.0')).toBe(3_010_099); + expect(mobileBuild('3.1.4')).toBe(3_010_499); + expect(mobileBuild('3.2.0-rc.0')).toBe(3_020_000); + expect(mobileBuild('3.2.0-rc.3')).toBe(3_020_003); }); - it('should prepend the new version to the front', () => { - const result = resolveArchivedVersions([archived('v2.9.0')], '3.0.0'); - - expect(result[0]).toEqual({ - label: 'v3.0.0', - url: 'https://docs.v3.0.0.archive.immich.app', - }); - expect(labels(result)).toEqual(['v3.0.0', 'v2.9.0']); + it('should be above the last hand-maintained build number', () => { + expect(mobileBuild('3.1.0')).toBeGreaterThan(3057); }); - it('should handle an empty list', () => { - expect(labels(resolveArchivedVersions([], '3.0.0'))).toEqual(['v3.0.0']); + it('should stay under the play store limit', () => { + expect(mobileBuild('99.99.99')).toBeLessThan(2_100_000_000); }); - it('should drop older patch releases of the new version minor', () => { - const versions = ['v3.0.4', 'v3.0.3', 'v3.0.2'].map(archived); + it('should increase across a release cycle', () => { + const versions = [ + '3.1.0', + '3.1.1', + '3.2.0-rc.0', + '3.2.0-rc.1', + '3.2.0', + '3.2.1', + '3.3.0-rc.0', + '4.0.0-rc.0', + '4.0.0', + ]; - expect(labels(resolveArchivedVersions(versions, '3.0.5'))).toEqual([ - 'v3.0.5', - ]); + const builds = versions.map((version) => mobileBuild(version)); + expect(builds).toEqual([...builds].sort((a, b) => a - b)); + expect(new Set(builds).size).toBe(builds.length); }); - it('should keep the latest patch of each older minor', () => { - const versions = ['v3.1.2', 'v3.1.1', 'v3.0.9', 'v2.0.0'].map(archived); - - expect(labels(resolveArchivedVersions(versions, '3.2.0'))).toEqual([ - 'v3.2.0', - 'v3.1.2', - 'v3.0.9', - 'v2.0.0', - ]); + it('should rank a release above its own candidates', () => { + expect(mobileBuild('3.2.0')).toBeGreaterThan(mobileBuild('3.2.0-rc.98')); }); - it('should keep an older minor when bumping a patch', () => { - const versions = ['v3.0.4', 'v3.0.3', 'v2.9.1', 'v2.9.0', 'v1.5.0'].map( - archived, - ); - - expect(labels(resolveArchivedVersions(versions, '3.0.5'))).toEqual([ - 'v3.0.5', - 'v2.9.1', - 'v1.5.0', - ]); + it('should rank a patch on an older line below the newer line', () => { + expect(mobileBuild('3.1.5')).toBeLessThan(mobileBuild('3.2.0')); }); - it('should replace a prerelease with its release', () => { - const versions = ['v3.0.0-rc.2', 'v3.0.0-rc.1', 'v2.9.0'].map(archived); - - expect(labels(resolveArchivedVersions(versions, '3.0.0'))).toEqual([ - 'v3.0.0', - 'v2.9.0', - ]); + it('should refuse versions it cannot encode', () => { + expect(() => mobileBuild('100.0.0')).toThrow('Cannot derive'); + expect(() => mobileBuild('3.100.0')).toThrow('Cannot derive'); + expect(() => mobileBuild('3.1.100')).toThrow('Cannot derive'); + expect(() => mobileBuild('3.1.0-rc.99')).toThrow('Cannot derive'); + expect(() => mobileBuild('3.1.0-beta')).toThrow('Cannot derive'); + expect(() => mobileBuild('3.1.0-rc.-1')).toThrow('Cannot derive'); + expect(() => mobileBuild('3.1.0-rc.x')).toThrow('Cannot derive'); }); }); diff --git a/packages/scripts/src/commands/release.ts b/packages/scripts/src/commands/release.ts index 68ef1dbf0e..2c0d82d5f6 100644 --- a/packages/scripts/src/commands/release.ts +++ b/packages/scripts/src/commands/release.ts @@ -26,7 +26,6 @@ const Files = { Docs: { Env: join(root, 'docs/docs/install/environment-variables.md'), Upgrading: join(root, 'docs/docs/install/upgrading.md'), - ArchivedVersions: join(root, 'docs/static/archived-versions.json'), }, Mobile: { Pubspec: join(root, 'mobile/pubspec.yaml'), @@ -35,7 +34,7 @@ const Files = { }, }; -export const handleRelease = ({ type, mobile }: ReleaseOptions) => { +export const handleRelease = ({ type }: ReleaseOptions) => { const versionRaw = getVersion(); const newVersionRaw = getNewVersion(versionRaw, type); const newVersion = semver.parse(normalize(newVersionRaw)); @@ -43,8 +42,7 @@ export const handleRelease = ({ type, mobile }: ReleaseOptions) => { throw new ReleaseInputError(); } const newVersionNoRc = `${newVersion.major}.${newVersion.minor}.${newVersion.patch}`; - const mobileBuild = getMobileBuild(); - const newMobileBuild = mobile ? mobileBuild + 1 : mobileBuild; + const newMobileBuild = getMobileBuild(newVersion); // pump versions everywhere @@ -93,13 +91,6 @@ export const handleRelease = ({ type, mobile }: ReleaseOptions) => { pump(Files.Docs.Upgrading, /:v\d+/, `:${major}`); } - // update archived versions list - const archivedFile = new JsonFile( - Files.Docs.ArchivedVersions, - ); - const versions = archivedFile.read(); - archivedFile.write(resolveArchivedVersions(versions, newVersionRaw)); - if (process.env.GITHUB_ENV) { // make available for following steps appendFileSync( @@ -189,14 +180,32 @@ export const getNewVersion = (versionRaw: string, type: string) => { return newVersionRaw; }; -const getMobileBuild = () => { - const pubspec = new TextFile(Files.Mobile.Pubspec).read(); - const match = pubspec.match(/^version: .*\+(\d+)$/m); - if (!match) { - throw new Error('Could not find mobile build number in pubspec.yaml'); +const RADIX = 100; +const STABLE = RADIX - 1; + +export const getMobileBuild = (version: SemVer) => { + const { major, minor, patch, prerelease } = version; + const candidate = prerelease[1]; + const digit = typeof candidate === 'number' ? candidate : STABLE; + + const valid = + major < RADIX && + minor < RADIX && + patch < RADIX && + (prerelease.length === 0 || digit < STABLE); + + if (!valid) { + throw new Error( + `Cannot derive a mobile build number from ${version.format()}`, + ); } - return Number(match[1]); + return ( + major * Math.pow(RADIX, 3) + + minor * Math.pow(RADIX, 2) + + patch * RADIX + + digit + ); }; const pump = (path: string, pattern: RegExp, replacement: string) => { @@ -205,50 +214,6 @@ const pump = (path: string, pattern: RegExp, replacement: string) => { file.write(update); }; -export interface ArchivedVersion { - label: string; - url: string; -} - -export const resolveArchivedVersions = ( - versions: ArchivedVersion[], - nextVersion: string, -): ArchivedVersion[] => { - const newVersion: ArchivedVersion = { - label: `v${nextVersion}`, - url: `https://docs.v${nextVersion}.archive.immich.app`, - }; - - let result = versions; - let lastVersion = asVersion(newVersion); - for (const item of versions) { - const version = asVersion(item); - // only keep the latest patch version for each minor release - if ( - lastVersion.major === version.major && - lastVersion.minor === version.minor && - lastVersion.patch >= version.patch - ) { - result = result.filter((item) => item.label !== version.label); - console.log( - `Removed ${version.label} (replaced with ${lastVersion.label})`, - ); - continue; - } - - lastVersion = version; - } - - return [newVersion, ...result]; -}; - -const asVersion = (item: ArchivedVersion) => { - const { label, url } = item; - const [version] = label.substring(1).split('-'); - const [major, minor, patch] = version.split('.').map(Number); - return { major, minor, patch, label, url }; -}; - const isPrerelease = (version: SemVer) => version.prerelease.length > 0; /** diff --git a/packages/scripts/src/types.ts b/packages/scripts/src/types.ts index 9bc23f87b5..586fdb8763 100644 --- a/packages/scripts/src/types.ts +++ b/packages/scripts/src/types.ts @@ -11,7 +11,7 @@ export const RELEASE_TYPES = [ ] as const; export type ReleaseType = (typeof RELEASE_TYPES)[number]; -export type ReleaseOptions = { type: string; mobile: boolean }; +export type ReleaseOptions = { type: string }; export class TextFile { constructor(private file: string) {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44d920aaa9..1e147c2502 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,16 +34,16 @@ importers: dependencies: '@docusaurus/core': specifier: ~3.10.0 - version: 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + version: 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/preset-classic': specifier: ~3.10.0 - version: 3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3) + version: 3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/theme-common': specifier: ~3.10.0 - version: 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + version: 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/theme-mermaid': specifier: ~3.10.0 - version: 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + version: 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@mdi/js': specifier: ^7.3.67 version: 7.4.47 @@ -52,25 +52,25 @@ importers: version: 1.6.1 '@mdx-js/react': specifier: ^3.0.0 - version: 3.1.1(@types/react@19.2.17)(react@19.2.8) + version: 3.1.1(@types/react@19.2.18)(react@19.2.8) autoprefixer: specifier: ^10.4.17 - version: 10.5.4(postcss@8.5.25) + version: 10.5.4(postcss@8.5.26) docusaurus-lunr-search: specifier: ^3.3.2 - version: 3.6.0(@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 3.6.0(@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) lunr: specifier: ^2.3.9 version: 2.3.9 postcss: specifier: ^8.4.25 - version: 8.5.25 + version: 8.5.26 prism-react-renderer: specifier: ^2.3.1 version: 2.4.1(react@19.2.8) raw-loader: specifier: ^4.0.2 - version: 4.0.2(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + version: 4.0.2(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) react: specifier: ^19.0.0 version: 19.2.8 @@ -79,20 +79,20 @@ importers: version: 19.2.8(react@19.2.8) tailwindcss: specifier: ^3.2.4 - version: 3.4.19(tsx@4.23.1)(yaml@2.9.0) + version: 3.4.19(tsx@4.23.9)(yaml@2.9.0) url: specifier: ^0.11.0 version: 0.11.4 devDependencies: '@docusaurus/module-type-aliases': specifier: ~3.10.0 - version: 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + version: 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/tsconfig': specifier: ^3.10.0 version: 3.10.2 '@docusaurus/types': specifier: ^3.10.0 - version: 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + version: 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@typescript/native': specifier: npm:typescript@^7.0.2 version: typescript@7.0.2 @@ -122,19 +122,19 @@ importers: version: link:../packages/sdk '@playwright/test': specifier: ^1.44.1 - version: 1.62.0 + version: 1.62.1 '@socket.io/component-emitter': specifier: ^3.1.2 version: 3.1.2 '@types/luxon': specifier: ^3.4.2 - version: 3.7.2 + version: 3.7.3 '@types/node': specifier: ^24.13.3 version: 24.13.3 '@types/pg': specifier: ^8.15.1 - version: 8.20.0 + version: 8.20.4 '@types/pngjs': specifier: ^6.0.4 version: 6.0.5 @@ -164,7 +164,7 @@ importers: version: 35.21.0 globals: specifier: ^17.0.0 - version: 17.7.0 + version: 17.9.0 luxon: specifier: ^3.4.4 version: 3.7.2 @@ -194,13 +194,13 @@ importers: version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.28.0 - version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + version: 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) utimes: specifier: ^5.2.1 version: 5.2.1(supports-color@8.1.1) vitest: specifier: ^4.0.0 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) packages/cli: dependencies: @@ -273,7 +273,7 @@ importers: version: 72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) globals: specifier: ^17.0.0 - version: 17.7.0 + version: 17.9.0 mock-fs: specifier: ^5.2.0 version: 5.5.0 @@ -288,13 +288,13 @@ importers: version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.58.0 - version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + version: 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) vite: specifier: ^8.0.0 - version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + version: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) vitest: specifier: ^4.0.0 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) vitest-fetch-mock: specifier: ^0.4.0 version: 0.4.5(vitest@4.1.10) @@ -306,16 +306,16 @@ importers: devDependencies: '@types/oidc-provider': specifier: ^9.0.0 - version: 9.5.0 + version: 9.11.0 jose: specifier: ^6.0.0 - version: 6.2.4 + version: 6.2.8 oidc-provider: specifier: ^9.0.0 - version: 9.10.0(supports-color@8.1.1) + version: 9.11.2(supports-color@8.1.1) tsx: specifier: ^4.20.6 - version: 4.23.1 + version: 4.23.9 packages/plugin-core: devDependencies: @@ -330,7 +330,7 @@ importers: version: link:../sdk esbuild: specifier: ^0.28.0 - version: 0.28.1 + version: 0.28.2 typescript: specifier: ^7.0.0 version: 7.0.2 @@ -352,7 +352,7 @@ importers: version: 24.13.3 esbuild: specifier: ^0.28.0 - version: 0.28.1 + version: 0.28.2 tsc-alias: specifier: ^1.8.16 version: 1.9.1 @@ -374,13 +374,13 @@ importers: version: 24.13.3 '@types/semver': specifier: ^7.7.1 - version: 7.7.1 + version: 7.8.0 vite: specifier: ^8.0.16 - version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + version: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) vitest: specifier: ^4.1.8 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) packages/sdk: dependencies: @@ -408,7 +408,7 @@ importers: version: 0.5.2 '@nestjs/bullmq': specifier: ^11.0.1 - version: 11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(bullmq@5.81.2(supports-color@8.1.1)) + version: 11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(bullmq@5.81.3(supports-color@8.1.1)) '@nestjs/common': specifier: ^11.0.4 version: 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) @@ -486,7 +486,7 @@ importers: version: 2.3.0(supports-color@8.1.1) bullmq: specifier: ^5.51.0 - version: 5.81.2(supports-color@8.1.1) + version: 5.81.3(supports-color@8.1.1) chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -534,10 +534,10 @@ importers: version: 5.11.1(supports-color@8.1.1) jose: specifier: ^6.0.0 - version: 6.2.4 + version: 6.2.8 js-yaml: specifier: ^5.0.0 - version: 5.2.2 + version: 5.2.3 jsonwebtoken: specifier: ^9.0.2 version: 9.0.3 @@ -576,7 +576,7 @@ importers: version: 5.5.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/swagger@11.4.6(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) nodemailer: specifier: ^9.0.0 - version: 9.0.3 + version: 9.0.4 openid-client: specifier: ^6.3.3 version: 6.8.4 @@ -621,7 +621,7 @@ importers: version: 4.8.3(supports-color@8.1.1) tailwindcss-preset-email: specifier: ^1.4.0 - version: 1.4.1(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0)) + version: 1.4.1(tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0)) thumbhash: specifier: ^0.1.1 version: 0.1.1 @@ -646,7 +646,7 @@ importers: version: 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@nestjs/cli': specifier: ^11.0.2 - version: 11.0.24(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/node@24.13.3)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(prettier@3.9.6)(uglify-js@3.19.3) + version: 11.0.24(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/node@24.13.3)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(prettier@3.9.6)(uglify-js@3.19.3) '@nestjs/schematics': specifier: ^11.0.0 version: 11.1.0(@typescript/typescript6@6.0.2)(chokidar@4.0.3)(prettier@3.9.6) @@ -655,7 +655,7 @@ importers: version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@nestjs/platform-express@11.1.28) '@swc/core': specifier: ^1.4.14 - version: 1.15.46(@swc/helpers@0.5.23) + version: 1.15.47(@swc/helpers@0.5.23) '@types/archiver': specifier: ^7.0.0 version: 7.0.0 @@ -688,10 +688,10 @@ importers: version: 9.0.10 '@types/lodash': specifier: ^4.14.197 - version: 4.17.24 + version: 4.17.25 '@types/luxon': specifier: ^3.6.2 - version: 3.7.2 + version: 3.7.3 '@types/mock-fs': specifier: ^4.13.1 version: 4.13.4 @@ -712,10 +712,10 @@ importers: version: 6.0.5 '@types/react': specifier: ^19.0.0 - version: 19.2.17 + version: 19.2.18 '@types/semver': specifier: ^7.5.8 - version: 7.7.1 + version: 7.8.0 '@types/supertest': specifier: ^7.0.0 version: 7.2.1 @@ -730,7 +730,7 @@ importers: version: typescript@7.0.2 '@vitest/coverage-v8': specifier: ^4.0.0 - version: 4.1.10(vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) eslint: specifier: ^10.0.0 version: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) @@ -745,7 +745,7 @@ importers: version: 72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) globals: specifier: ^17.0.0 - version: 17.7.0 + version: 17.9.0 mock-fs: specifier: ^5.2.0 version: 5.5.0 @@ -766,31 +766,31 @@ importers: version: 7.2.2(supports-color@8.1.1) tailwindcss: specifier: ^3.4.0 - version: 3.4.19(tsx@4.23.1)(yaml@2.9.0) + version: 3.4.19(tsx@4.23.9)(yaml@2.9.0) testcontainers: specifier: ^12.0.0 - version: 12.0.4(supports-color@8.1.1) + version: 12.1.0(supports-color@8.1.1) typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.28.0 - version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + version: 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) unplugin-swc: specifier: ^1.4.5 - version: 1.5.9(@swc/core@1.15.46(@swc/helpers@0.5.23))(rollup@4.62.0) + version: 1.5.10(@swc/core@1.15.47(@swc/helpers@0.5.23))(rollup@4.62.0) vite-tsconfig-paths: specifier: ^6.0.0 - version: 6.1.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 6.1.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1)(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) vitest: specifier: ^3.0.0 - version: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) web: dependencies: '@formatjs/icu-messageformat-parser': specifier: ^3.0.0 - version: 3.5.15 + version: 3.5.16 '@immich/justified-layout-wasm': specifier: ^0.4.3 version: 0.4.3 @@ -798,8 +798,8 @@ importers: specifier: workspace:* version: link:../packages/sdk '@immich/ui': - specifier: ^0.83.0 - version: 0.83.0(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + specifier: ^0.85.0 + version: 0.85.0(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) '@mapbox/mapbox-gl-rtl-text': specifier: 0.4.0 version: 0.4.0 @@ -808,25 +808,25 @@ importers: version: 7.4.47 '@noble/hashes': specifier: ^2.2.0 - version: 2.2.0 + version: 2.3.0 '@photo-sphere-viewer/core': specifier: ^5.14.0 - version: 5.15.0 + version: 5.15.1 '@photo-sphere-viewer/equirectangular-video-adapter': specifier: ^5.14.0 - version: 5.15.0(@photo-sphere-viewer/core@5.15.0)(@photo-sphere-viewer/video-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)) + version: 5.15.1(@photo-sphere-viewer/core@5.15.1)(@photo-sphere-viewer/video-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1)) '@photo-sphere-viewer/markers-plugin': specifier: ^5.14.0 - version: 5.15.0(@photo-sphere-viewer/core@5.15.0) + version: 5.15.1(@photo-sphere-viewer/core@5.15.1) '@photo-sphere-viewer/resolution-plugin': specifier: ^5.14.0 - version: 5.15.0(@photo-sphere-viewer/core@5.15.0)(@photo-sphere-viewer/settings-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)) + version: 5.15.1(@photo-sphere-viewer/core@5.15.1)(@photo-sphere-viewer/settings-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1)) '@photo-sphere-viewer/settings-plugin': specifier: ^5.14.0 - version: 5.15.0(@photo-sphere-viewer/core@5.15.0) + version: 5.15.1(@photo-sphere-viewer/core@5.15.1) '@photo-sphere-viewer/video-plugin': specifier: ^5.14.0 - version: 5.15.0(@photo-sphere-viewer/core@5.15.0) + version: 5.15.1(@photo-sphere-viewer/core@5.15.1) '@types/geojson': specifier: ^7946.0.16 version: 7946.0.16 @@ -835,7 +835,7 @@ importers: version: 0.42.0 '@zoom-image/svelte': specifier: ^0.3.0 - version: 0.3.9(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 0.3.9(svelte@5.56.8(@typescript-eslint/types@8.66.0)) dom-to-image: specifier: ^2.6.0 version: 2.6.0 @@ -859,10 +859,10 @@ importers: version: 1.5.11 hls.js: specifier: ^1.6.16 - version: 1.6.16 + version: 1.6.17 intl-messageformat: specifier: ^11.0.0 - version: 11.2.12 + version: 11.2.13 justified-layout: specifier: ^4.1.0 version: 4.1.0 @@ -886,7 +886,7 @@ importers: version: 1.5.4 simple-icons: specifier: ^16.0.0 - version: 16.27.1 + version: 16.28.0 socket.io-client: specifier: ~4.8.0 version: 4.8.3(supports-color@8.1.1) @@ -895,16 +895,16 @@ importers: version: 5.2.2 svelte-i18n: specifier: ^4.0.1 - version: 4.0.1(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 4.0.1(svelte@5.56.8(@typescript-eslint/types@8.66.0)) svelte-jsoneditor: specifier: ^3.10.0 - version: 3.13.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 3.13.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) svelte-maplibre: specifier: ^1.2.5 - version: 1.3.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 1.3.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) svelte-persisted-store: specifier: ^0.12.0 - version: 0.12.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 0.12.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) tabbable: specifier: ^6.2.0 version: 6.5.0 @@ -913,7 +913,7 @@ importers: version: 3.6.0 tailwind-variants: specifier: ^3.2.2 - version: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3) + version: 3.3.1(tailwind-merge@3.6.0)(tailwindcss@4.3.3) thumbhash: specifier: ^0.1.1 version: 0.1.1 @@ -938,31 +938,31 @@ importers: version: 3.1.2 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.10(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))) + version: 3.0.10(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))) '@sveltejs/enhanced-img': specifier: ^0.11.0 - version: 0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@sveltejs/kit': specifier: ^2.56.1 - version: 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@sveltejs/vite-plugin-svelte': specifier: 7.2.0 - version: 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@tailwindcss/vite': specifier: ^4.2.4 - version: 4.3.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.3.3(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@testing-library/jest-dom': specifier: ^7.0.0 version: 7.0.0(@testing-library/dom@10.4.1) '@testing-library/svelte': specifier: ^5.2.8 - version: 5.4.2(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))(vitest@4.1.10) + version: 5.4.2(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))(vitest@4.1.10) '@testing-library/user-event': specifier: ^14.5.2 - version: 14.6.1(@testing-library/dom@10.4.1) + version: 14.6.3(@testing-library/dom@10.4.1) '@trivago/prettier-plugin-sort-imports': specifier: ^6.0.2 - version: 6.0.2(prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)))(prettier@3.9.6)(supports-color@8.1.1)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 6.0.2(prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)))(prettier@3.9.6)(supports-color@8.1.1)(svelte@5.56.8(@typescript-eslint/types@8.66.0)) '@types/chromecast-caf-sender': specifier: ^1.0.11 version: 1.0.11 @@ -977,7 +977,7 @@ importers: version: 4.17.12 '@types/luxon': specifier: ^3.4.2 - version: 3.7.2 + version: 3.7.3 '@types/qrcode': specifier: ^1.5.5 version: 1.5.6 @@ -1004,7 +1004,7 @@ importers: version: 7.0.2(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-svelte: specifier: ^3.12.4 - version: 3.22.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 3.22.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) eslint-plugin-unicorn: specifier: ^72.0.0 version: 72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) @@ -1013,7 +1013,7 @@ importers: version: 1.4.2 globals: specifier: ^17.0.0 - version: 17.7.0 + version: 17.9.0 prettier: specifier: ^3.7.4 version: 3.9.6 @@ -1022,19 +1022,19 @@ importers: version: 4.2.0(prettier@3.9.6) prettier-plugin-svelte: specifier: ^4.0.0 - version: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)) rollup-plugin-visualizer: specifier: ^7.0.0 - version: 7.0.1(rolldown@1.1.5)(rollup@4.62.0) + version: 7.0.1(rolldown@1.2.3)(rollup@4.62.0) svelte: specifier: 5.56.8 - version: 5.56.8(@typescript-eslint/types@8.65.0) + version: 5.56.8(@typescript-eslint/types@8.66.0) svelte-check: specifier: ^4.4.6 - version: 4.7.3(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 4.7.4(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.66.0)) svelte-eslint-parser: specifier: ^1.3.3 - version: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + version: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) tailwindcss: specifier: ^4.2.4 version: 4.3.3 @@ -1043,13 +1043,13 @@ importers: version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.45.0 - version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + version: 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) vite: specifier: ^8.0.0 - version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + version: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) vitest: specifier: ^4.0.0 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) packages: @@ -2336,15 +2336,9 @@ packages: resolution: {integrity: sha512-xx0W3eav2uW1NRIpuHJWNwLTC15xPNjU4Uxi9NSnd3swYC96BE3vFiT93SD8s24kmAAWNwgZwfZ2fghGZ01Lcw==} engines: {node: '>=20.0'} - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} engines: {node: '>=12'} @@ -2357,8 +2351,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.28.1': - resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -2375,8 +2369,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.28.1': - resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -2393,8 +2387,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.28.1': - resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -2411,8 +2405,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.28.1': - resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -2429,8 +2423,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.28.1': - resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -2447,8 +2441,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.28.1': - resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -2465,8 +2459,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.28.1': - resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -2483,8 +2477,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.28.1': - resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -2501,8 +2495,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.28.1': - resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -2519,8 +2513,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.28.1': - resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -2537,8 +2531,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.28.1': - resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -2555,8 +2549,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.28.1': - resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -2573,8 +2567,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.28.1': - resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -2591,8 +2585,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.28.1': - resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -2609,8 +2603,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.28.1': - resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -2627,8 +2621,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.28.1': - resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -2645,8 +2639,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.28.1': - resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -2657,8 +2651,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.28.1': - resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -2675,8 +2669,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.28.1': - resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -2687,8 +2681,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.28.1': - resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -2705,8 +2699,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.28.1': - resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -2717,8 +2711,8 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.28.1': - resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -2735,8 +2729,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.28.1': - resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -2753,8 +2747,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.28.1': - resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -2771,8 +2765,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.28.1': - resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -2789,8 +2783,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.28.1': - resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -2874,8 +2868,8 @@ packages: '@formatjs/icu-messageformat-parser@2.11.4': resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} - '@formatjs/icu-messageformat-parser@3.5.15': - resolution: {integrity: sha512-5o4grXKotAB3JqQuisLApHG43g17N+paoRTa92Jiz35Zvfemq0cVf4EDvuxyHAzmsJji7igaEowicLO/VmfJ8Q==} + '@formatjs/icu-messageformat-parser@3.5.16': + resolution: {integrity: sha512-kl6b/4D56gjGZi4ZewSmvXbalHwjOUI5ogEHPZqw42goeXTTrL7/yuPzvdrvr0QigDtvaOeb+UeMf62jks43Yg==} '@formatjs/icu-skeleton-parser@1.8.16': resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} @@ -3110,8 +3104,8 @@ packages: resolution: {integrity: sha512-O1SJ+BbeFVsUTF4af1MfagJZM+lPgLjI8lQ3SZNjpo8SGJReSbUl2ii03OKuGni/G0yp2GnRLpOTNSHYGtVrcg==} hasBin: true - '@immich/ui@0.83.0': - resolution: {integrity: sha512-Xh3R3yhn8/Qyq8lGWIiVmturA+dHXtmW/h++AGb2UAwS/58qu7ssocO/HWIamaSpkgjPGotlWpPIpUJq1zh9FQ==} + '@immich/ui@0.85.0': + resolution: {integrity: sha512-KCAEtVGexZ1G+nuT25bv1QS6Rc2wOlLymp4qDBZVqMxu3o3RWpUzafRHvZt5g4FnlAgfHZdXAIHVVrhJzkrlDA==} peerDependencies: '@sveltejs/kit': ^2.13.0 svelte: ^5.0.0 @@ -3580,12 +3574,6 @@ packages: '@namnode/store@0.1.0': resolution: {integrity: sha512-4NGTldxKcmY0UuZ7OEkvCjs8ZEoeYB6M2UwMu74pdLiFMKxXbj9HdNk1Qn213bxX1O7bY5h+PLh5DZsTURZkYA==} - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - '@nestjs/bull-shared@11.0.4': resolution: {integrity: sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==} peerDependencies: @@ -3735,8 +3723,8 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} - '@noble/hashes@2.2.0': - resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} + '@noble/hashes@2.3.0': + resolution: {integrity: sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==} engines: {node: '>= 20.19.0'} '@nodelib/fs.scandir@2.1.5': @@ -3968,8 +3956,8 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 - '@oxc-project/types@0.139.0': - resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@oxc-project/types@0.143.0': + resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} @@ -4099,35 +4087,35 @@ packages: resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} engines: {node: '>=20.0.0'} - '@photo-sphere-viewer/core@5.15.0': - resolution: {integrity: sha512-Mf72l3R8iBf+Im5duv7IVkX3uiKU1BT6bzzXmdE43KahQgNftt9pYKqTzN1YjaYLAzWDBCFZj/bEK1dIZXk9Gg==} + '@photo-sphere-viewer/core@5.15.1': + resolution: {integrity: sha512-nhJviczUB1xiAT/I7mGba9iDIw7DvsiK34hbR9t7kIrg9MySG33uA+a4QUk2dEOP5PjcKMj58wod8I/dQ7ePiw==} - '@photo-sphere-viewer/equirectangular-video-adapter@5.15.0': - resolution: {integrity: sha512-Gro1wUA5qTla71uSeGlQFr9BenNXrI9Xee4a+dveafqrZJ35/n+nDhxrc8I4osykyvC6Foc0qgksAsaNsMKX/w==} + '@photo-sphere-viewer/equirectangular-video-adapter@5.15.1': + resolution: {integrity: sha512-510h1cDBAe+qoHVrR5IwUE/jnmzz0lA9mXHN7yzJDsrixPRh67gXEB3KIEc2Cgj/B1u1D2WK2W7OwhkQ40AX0A==} peerDependencies: - '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/video-plugin': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 + '@photo-sphere-viewer/video-plugin': 5.15.1 - '@photo-sphere-viewer/markers-plugin@5.15.0': - resolution: {integrity: sha512-r1Kv4BkvcOf+77QU7k0pHzZ3O0IrX+ff7arttmb0D81HQHRl/fUcE+tXu+mGDj9B1zuSi1LcvKOsUI9m96owNw==} + '@photo-sphere-viewer/markers-plugin@5.15.1': + resolution: {integrity: sha512-57WiHy8fcQfwid9x07ADiQJGxzcFxclbR2yIqeggzfPDguwJlCKwX5tj8brTRqwouUiHpkb80QWxHByxt45LcA==} peerDependencies: - '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 - '@photo-sphere-viewer/resolution-plugin@5.15.0': - resolution: {integrity: sha512-8Rm9r9WJX5m8N0Q7ezUEHUgxWybroj6HhfaksINUm7o8WegqZ+Pp2izEEoXI57kkjfudYOp7abrwA/J3I+QRrA==} + '@photo-sphere-viewer/resolution-plugin@5.15.1': + resolution: {integrity: sha512-Sp14tgFv1XRkXbsFO+dWSgfwGjxB4ibsB+wXe64M4RI/NSDu+1Uzqtxj85Vbz7gOw3CaP1e8acXrd/IEg6CEHg==} peerDependencies: - '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/settings-plugin': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 + '@photo-sphere-viewer/settings-plugin': 5.15.1 - '@photo-sphere-viewer/settings-plugin@5.15.0': - resolution: {integrity: sha512-xZot+fT5R+Jy7D/QEVoylIMg/R+l/iSBykQ1v8wqGkSrUqJdVkNv3w3ksycb9BYFt/78P8HU7AGvJgIF6J3EFQ==} + '@photo-sphere-viewer/settings-plugin@5.15.1': + resolution: {integrity: sha512-0FkCUUEsdTa4iLZu9nVLsjuh459hKKZeh+JiXfPzfrGLcLODLUwJugDjFHe/4KH7mmW9n7KbSUWBD8POvkUqqA==} peerDependencies: - '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 - '@photo-sphere-viewer/video-plugin@5.15.0': - resolution: {integrity: sha512-RG70vRu+suV7QT1WbnxVknm8KVU26wCZx4NUofFAENYSvyldlRxmVK3XqCh1WChWhcjrXqj3woz4vnzrykVOzQ==} + '@photo-sphere-viewer/video-plugin@5.15.1': + resolution: {integrity: sha512-jrZh2BGZA8BGzNj+gzDntfX8bd4kt0vIBsyAslSgPpkJUTRKxZ2nTIp2aP1OQ4EtpP3g4kbqrIAe6Avdn6pr0A==} peerDependencies: - '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 '@photostructure/tz-lookup@11.5.0': resolution: {integrity: sha512-0DVFriinZ7TeOnm9ytXeSL3NMFU87ZqMjgbPNkd8LgHFLcPg1BDyM1eewFYs+pPM+62S4fSP9Mtgijmn+6y95w==} @@ -4140,8 +4128,8 @@ packages: resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} - '@playwright/test@1.62.0': - resolution: {integrity: sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==} + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} engines: {node: '>=20'} hasBin: true @@ -4380,97 +4368,92 @@ packages: '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 - '@rolldown/binding-android-arm64@1.1.5': - resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + '@rolldown/binding-android-arm64@1.2.3': + resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.1.5': - resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + '@rolldown/binding-darwin-arm64@1.2.3': + resolution: {integrity: sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.5': - resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + '@rolldown/binding-darwin-x64@1.2.3': + resolution: {integrity: sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.1.5': - resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + '@rolldown/binding-freebsd-x64@1.2.3': + resolution: {integrity: sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + resolution: {integrity: sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.1.5': - resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + '@rolldown/binding-linux-arm64-gnu@1.2.3': + resolution: {integrity: sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.1.5': - resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + '@rolldown/binding-linux-arm64-musl@1.2.3': + resolution: {integrity: sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + '@rolldown/binding-linux-ppc64-gnu@1.2.3': + resolution: {integrity: sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.5': - resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + '@rolldown/binding-linux-s390x-gnu@1.2.3': + resolution: {integrity: sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.5': - resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + '@rolldown/binding-linux-x64-gnu@1.2.3': + resolution: {integrity: sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.1.5': - resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + '@rolldown/binding-linux-x64-musl@1.2.3': + resolution: {integrity: sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.1.5': - resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + '@rolldown/binding-openharmony-arm64@1.2.3': + resolution: {integrity: sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.1.5': - resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.1.5': - resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + '@rolldown/binding-win32-arm64-msvc@1.2.3': + resolution: {integrity: sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.5': - resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + '@rolldown/binding-win32-x64-msvc@1.2.3': + resolution: {integrity: sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -4675,8 +4658,8 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.11': - resolution: {integrity: sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==} + '@sveltejs/acorn-typescript@1.0.12': + resolution: {integrity: sha512-J1jNYG23QWd67UfrQSFHtjhV37r9mVi0gdc12A3MWPldOjRK35Xk+um+qACPVjgw3AleiqoyEAhok8Wm3q46NA==} peerDependencies: acorn: ^8.9.0 @@ -4692,8 +4675,8 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || >=7.0.0 - '@sveltejs/kit@2.70.1': - resolution: {integrity: sha512-nY9SPHGOZro3doud9vZXDBwl9tCZIouuJztjgSHs6PAIrv9M/z5O7eOhPV5xU7CgVHA976Jwu3BA1hIFvXztkA==} + '@sveltejs/kit@2.70.2': + resolution: {integrity: sha512-RzRoRpuR2KXqc5yMO0akQHDZeT4AslOlznGITURsqHaVbtyYP4Wn3eE3gxj9JcDyNYO0crkxhdwFHc+2vkVm6w==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -4708,8 +4691,8 @@ packages: typescript: optional: true - '@sveltejs/load-config@0.2.0': - resolution: {integrity: sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==} + '@sveltejs/load-config@0.2.2': + resolution: {integrity: sha512-K7dsJDQxBOF+f+epuhMactcjK2VP4MRkLKtwSykNtEI+cKVEyzrmmhQ1pmoxI800m4JKcyJ05L2M4yPwAGiBNw==} engines: {node: '>= 18.0.0'} '@sveltejs/vite-plugin-svelte@7.2.0': @@ -4797,86 +4780,86 @@ packages: resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==} engines: {node: '>=14'} - '@swc/core-darwin-arm64@1.15.46': - resolution: {integrity: sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q==} + '@swc/core-darwin-arm64@1.15.47': + resolution: {integrity: sha512-GsoMtan3ojGGMGFbl31mmRu5ctZ56re8grGE8mO/OHJ8O+JRkzod02fe7X6ZQ8JvamA3imkEkx/h3u+vsOgPgA==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.46': - resolution: {integrity: sha512-4Tj4ppVIPCmUMpmGFiGtyEriwLyJ+yi/US4WfBrP/ok8COGddDZXLEzQETnKyK46mjvr1v0jevrS23zjoff7vA==} + '@swc/core-darwin-x64@1.15.47': + resolution: {integrity: sha512-leTi7Rx3KF4zcC637iqWgk9SoV8VXAD8ppQYXsep63px5A/UftOcxLN1pmr8Z1si/YvX90ompP/rHgpYkgwXWg==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.46': - resolution: {integrity: sha512-i8tUGnNjyOgMmfmgFSg4aeJLQoFyfpIHK5FjpQAwpRyQIqEUB2w1e8zIDQzY1WhOxx8NoS1S5iUL813Un4Sf5A==} + '@swc/core-linux-arm-gnueabihf@1.15.47': + resolution: {integrity: sha512-hBqHuoWKKIsKmDBn9qVeWqj5GWZhtlcczVaqQmNRXsDfq+voR5CxKRfamA367QjJXtceYuliLFfEL8QsskRM2g==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.46': - resolution: {integrity: sha512-c0OnhqzdhfOvv6qhNCcByepB+sNYOGZyhtr2Qa6ZCHvAWTYhSRw4j/u92Stue9PbZ/6q74b9nHzi76+kVzqQHQ==} + '@swc/core-linux-arm64-gnu@1.15.47': + resolution: {integrity: sha512-TBxvRz+B4K205TWHHZxWVxkC2RFNP/Mz3PNcECBos5PsKwxjg3QSJzdoebr0VCf0Bfh8HOPldKxAP/8XkFe9gA==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [glibc] - '@swc/core-linux-arm64-musl@1.15.46': - resolution: {integrity: sha512-imyRpNEcUzFQFV2LE4jL68ErvmKEuZCbvZru77iQREunJ+bR4i658cupTgtG1mLYM3F1Tzy3Sb9xYb02KghWTg==} + '@swc/core-linux-arm64-musl@1.15.47': + resolution: {integrity: sha512-3Yu3Uq/VgytqsPjTMbkPU1ExADytbdWbruJYhA584E9jrpE2Ki+R6VVPoZCeAVk1Cb7QxcRTgblw6bSa6a/R+w==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [musl] - '@swc/core-linux-ppc64-gnu@1.15.46': - resolution: {integrity: sha512-ctEfcl/HcUeomK33cbySiHZm98GEDIxTm1EkpBsYCiHxElYBzvTXVeuQT2YwbUXn9XCrjiw4ipyUNk33k26qRg==} + '@swc/core-linux-ppc64-gnu@1.15.47': + resolution: {integrity: sha512-wfdMi5IaOaNtmh2/6geRoxIdNfqylUZFdtzTKS655y1axWfIWyx7As74vv0wVdjeCIZ3WmCI9odDd4rUttXOSQ==} engines: {node: '>=10'} cpu: [ppc64] os: [linux] libc: [glibc] - '@swc/core-linux-s390x-gnu@1.15.46': - resolution: {integrity: sha512-DxlMdnt84TtRVTv7WL/thWyz9+QU8QZNNoAP9rrk0P68LziuhfePp8MjQ44zIprpTHTsEwyziIuGUUN5iSC1bQ==} + '@swc/core-linux-s390x-gnu@1.15.47': + resolution: {integrity: sha512-3hHYBY0yx8Ez7GMRrkhXHQzMdR5IZA6Wq5Ee4svlgwvSECLpnAJ9+0AimEGUFDvuLwE7nV/2+PYe8+Nm4rvNcQ==} engines: {node: '>=10'} cpu: [s390x] os: [linux] libc: [glibc] - '@swc/core-linux-x64-gnu@1.15.46': - resolution: {integrity: sha512-SKxI7J6t90XPl8hRUqtJi9NfGdunN/E/vZMc7Bc0figeRdOPDBT+Tm8g7cx9xM0T0mewh2l+8dewa3Am27/P+A==} + '@swc/core-linux-x64-gnu@1.15.47': + resolution: {integrity: sha512-TjfhjgP/jGCfFHYC3JQPhJA1HwErbIJ9JfREDc1KNkvY6P0LodCgKVIlQ5deeTbkG7ih3bF5PHJLuLpaZjdRyQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [glibc] - '@swc/core-linux-x64-musl@1.15.46': - resolution: {integrity: sha512-qj9T6B7bosI0VEsrWOVXZN1OXxS8Tp63ywyrLxNdOycnUtLdkgYcoBsN5y8ImnDDsnwrEWZOy1e+J4xSe7mA3Q==} + '@swc/core-linux-x64-musl@1.15.47': + resolution: {integrity: sha512-CQpS8Ge/avfjZd0UEwG/sds83Uu32deQXcV1Jo3jD0mmvQQqtYAjpsDZXugmheeAwmt+YIuoVtVHro8LMYHqsQ==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [musl] - '@swc/core-win32-arm64-msvc@1.15.46': - resolution: {integrity: sha512-8p7l4c3LU+eA5g9Et1JPhNeMC1oQwXTGU+uah8DPIBX7YXzqswvaBtyKVmXefVGi/DJU1x3YJsc3mbAp9aWzSQ==} + '@swc/core-win32-arm64-msvc@1.15.47': + resolution: {integrity: sha512-0W8IKHsUTYiT7G2RqtOoVWk+89yzZikIiDUb/sCK6BmQDBhN91hQSfyUtW12jhEWLzYgcfmisfsZrmZE+84U1A==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.46': - resolution: {integrity: sha512-tUEnfr3Bn9u6FOjUb3PN9p+09qZC2j+wNDLKHzXXZn22rqGcUqR/ohCRSS+nG9B9+X+U+3FewNEHJkTmdIvMjQ==} + '@swc/core-win32-ia32-msvc@1.15.47': + resolution: {integrity: sha512-ZIp49d2Z4/ka2jO9otOg4hDvTdPmp86kVOgS2M5FCPI7eKKZ1W0boxWn+8XeZrfERtFGW0AlMRm4JhlJa7l3NA==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.46': - resolution: {integrity: sha512-Vux7UDzBJYQggSuPfcl2w9iu+IJpgpRCxHzgCaVkELnAXAE4XZMOTX9HNcaNiwfeIDqdu2rkr69RuDm6wY8neA==} + '@swc/core-win32-x64-msvc@1.15.47': + resolution: {integrity: sha512-2h8Iek95vnixkBRCo+H8p09+Q5ll2NgSMFrWTy0iKt7+/t+8/T5mBpiT6c0ZxSS7wcWjwZ9sGZkK70tTSYHdDw==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.46': - resolution: {integrity: sha512-Ri3em2mBpq3h2zSPliCYl63otDGqek8PPEfv2nWgRQEbZ/VBCNyypVTVQ6cEbTCXBhy+WE2T3fQb08moIyuYaw==} + '@swc/core@1.15.47': + resolution: {integrity: sha512-FbsO5JcfOjfH38W/rohBRBweJeERsAuIP4f377lmkmxTcq9exjtx4SkRuZY5CdfhR2CBVwDIJegBpJDffwNsOg==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -5020,8 +5003,8 @@ packages: vitest: optional: true - '@testing-library/user-event@14.6.1': - resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + '@testing-library/user-event@14.6.3': + resolution: {integrity: sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==} engines: {node: '>=12', npm: '>=6'} peerDependencies: '@testing-library/dom': '>=7.21.4' @@ -5061,9 +5044,6 @@ packages: '@turf/invariant@7.3.5': resolution: {integrity: sha512-ZVIvsBvjr8lO7WxC5zYNjRsjSDvyGvWkJMjuWaJjTU8x+1tmfNnw3gDX/TI2Sit83gcRYLYkNo23lB/udqx/Hg==} - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - '@types/accepts@1.3.7': resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} @@ -5345,11 +5325,11 @@ packages: '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - '@types/lodash@4.17.24': - resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} - '@types/luxon@3.7.2': - resolution: {integrity: sha512-gW+Oib+vUtGJBtNC8V9Reww0oIpusw+4m81uncg9REGZAJfqOQHfo/nkabnc7w0QReXyPqjrbWMJk6NuAkiX3Q==} + '@types/luxon@3.7.3': + resolution: {integrity: sha512-pE7BSbKHiojpl4v7iEzdfFLXXJaH5RPJxI3Wr4x3HU59l83PJfhcUx4mGPWq245+DCAENAzRF/+ZoYr2tJCe/g==} '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -5387,8 +5367,8 @@ packages: '@types/nodemailer@8.0.1': resolution: {integrity: sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==} - '@types/oidc-provider@9.5.0': - resolution: {integrity: sha512-eEzCRVTSqIHD9Bo/qRJ4XQWQ5Z/zBcG+Z2cGJluRsSuWx1RJihqRyPxhIEpMXTwPzHYRTQkVp7hwisQOwzzSAg==} + '@types/oidc-provider@9.11.0': + resolution: {integrity: sha512-wZMEmCGh8WGKYSUpxJgpPLIKTe4972OJUlWXaNzy5+vvnU097i8ijzApgHLihdg56cQ+PV+USYYcnh3W8kVISA==} '@types/parse5@5.0.3': resolution: {integrity: sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==} @@ -5399,8 +5379,8 @@ packages: '@types/pg@8.15.6': resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} - '@types/pg@8.20.0': - resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/pg@8.20.4': + resolution: {integrity: sha512-Jz7UDOlIiFJuacC0TlBoLyNtmwlA/wpIyPDd3tvUqlRM+HzkWy2xUgpFpaXtbfTAFF6sIGq5lsCDBdJnhky1Xg==} '@types/picomatch@4.0.3': resolution: {integrity: sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==} @@ -5429,8 +5409,8 @@ packages: '@types/react-router@5.1.20': resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} @@ -5441,8 +5421,8 @@ packages: '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} - '@types/semver@7.7.1': - resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/semver@7.8.0': + resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==} '@types/send@0.17.6': resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} @@ -5507,63 +5487,63 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.65.0': - resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} + '@typescript-eslint/eslint-plugin@8.66.0': + resolution: {integrity: sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.65.0 + '@typescript-eslint/parser': ^8.66.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.65.0': - resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} + '@typescript-eslint/parser@8.66.0': + resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.65.0': - resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} + '@typescript-eslint/project-service@8.66.0': + resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.65.0': - resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} + '@typescript-eslint/scope-manager@8.66.0': + resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.65.0': - resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} + '@typescript-eslint/tsconfig-utils@8.66.0': + resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.65.0': - resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} + '@typescript-eslint/type-utils@8.66.0': + resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.65.0': - resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} + '@typescript-eslint/types@8.66.0': + resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.65.0': - resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} + '@typescript-eslint/typescript-estree@8.66.0': + resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.65.0': - resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} + '@typescript-eslint/utils@8.66.0': + resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.65.0': - resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} + '@typescript-eslint/visitor-keys@8.66.0': + resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript/typescript-aix-ppc64@7.0.2': @@ -6139,9 +6119,9 @@ packages: bare-abort-controller: optional: true - bare-fs@4.7.4: - resolution: {integrity: sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==} - engines: {bare: '>=1.16.0'} + bare-fs@4.8.0: + resolution: {integrity: sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==} + engines: {bare: '>=1.28.0'} peerDependencies: bare-buffer: '*' peerDependenciesMeta: @@ -6165,8 +6145,8 @@ packages: bare-events: optional: true - bare-url@2.4.5: - resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==} + bare-url@2.5.1: + resolution: {integrity: sha512-cD5ciQuKlx+eumTCfqbfiL+fhQm+dHbVNB/cX4+d+I/nx4JsVop9VoFEPAs5RJ6I84QR6bZIBjpd2kjDOYeWcg==} base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -6283,8 +6263,8 @@ packages: resolution: {integrity: sha512-02yxLeyxF4dNl6SlY6/5HfRSrSdZ/sCPoxy2kZNP5dZZX8LSAD9aE2gtJIUgWrsQTiMPl3mxESyrobSwvRGisQ==} engines: {node: '>=18.20'} - bullmq@5.81.2: - resolution: {integrity: sha512-Hi9GaVCC6HE9bQP65j/FNv1aL1fcEukTF99ezS5pl1Ud+joCFpNWiPCV45mWdkEmpuacS0XJdMMFGKPIHCwoPg==} + bullmq@5.81.3: + resolution: {integrity: sha512-Q7uEH2G92rVjX3Yl2qw3+6VEO15uxehC6DfeTHQBQTehddcRDSDuquD9zuvDjxiCffXsPUaBwrRlvlbVcgKs7g==} engines: {node: '>=12.22.0'} peerDependencies: redis: '>=5.0.0' @@ -7268,8 +7248,8 @@ packages: engines: {node: '>= 16.0.0'} hasBin: true - devalue@5.8.2: - resolution: {integrity: sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==} + devalue@5.9.0: + resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -7525,8 +7505,8 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.28.1: - resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} hasBin: true @@ -8016,6 +7996,10 @@ packages: resolution: {integrity: sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==} engines: {node: '>=18'} + fuse.js@7.5.0: + resolution: {integrity: sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow==} + engines: {node: '>=10'} + gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} @@ -8129,8 +8113,8 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} - globals@17.7.0: - resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} + globals@17.9.0: + resolution: {integrity: sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==} engines: {node: '>=18'} globalyzer@0.1.0: @@ -8278,8 +8262,8 @@ packages: hls-video-element@1.5.11: resolution: {integrity: sha512-tJJ65/52CDxj8XFyIve6zT9nVVdUIc6mqvKR25X0ycPKHk07rpjp4xxVteeCefDUBSf/tFLhlICFmn3KWj37xA==} - hls.js@1.6.16: - resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} + hls.js@1.6.17: + resolution: {integrity: sha512-NUplVGVuc1hSPwdB/9/cbRkUmLrYi75/hqiXKdA+l300pJNxDu96R7jRb2imDzWJqIUF4I5ThmAdp9GvOCXsuQ==} hogan.js@3.0.2: resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==} @@ -8515,8 +8499,8 @@ packages: intl-messageformat@10.7.18: resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} - intl-messageformat@11.2.12: - resolution: {integrity: sha512-KW70Xxfcvy7vV3qODfvShWkFDPMqKDAa4N+hSyVBWGNtVhTUFYaqlD/l88DaYPKiVcPP4rPQ3qnH7i5K82Mg7g==} + intl-messageformat@11.2.13: + resolution: {integrity: sha512-JaPaE6TIX+TAS5XLhDUh41geLw4QfBHX4s5pW8Km+L9fVC8HzB9yOuhbh4EMR/F1+8C6b9qk4763Cv+LdOG1kg==} invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -8779,8 +8763,8 @@ packages: joi@17.13.4: resolution: {integrity: sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==} - jose@6.2.4: - resolution: {integrity: sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==} + jose@6.2.8: + resolution: {integrity: sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==} js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -8799,8 +8783,8 @@ packages: resolution: {integrity: sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==} hasBin: true - js-yaml@5.2.2: - resolution: {integrity: sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==} + js-yaml@5.2.3: + resolution: {integrity: sha512-n+mUVyUX5bVv7G/G2zyIHOhdxfuU1dY2NOFzTQUWiMUbFss8b57NFlgCCaggU78wSw5KVS9cllzeLyzyR+n5nw==} hasBin: true jsdom@26.1.0: @@ -9732,8 +9716,8 @@ packages: resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} hasBin: true - msgpackr@2.0.4: - resolution: {integrity: sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==} + msgpackr@2.0.5: + resolution: {integrity: sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==} multer@2.2.0: resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} @@ -9763,13 +9747,13 @@ packages: nan@2.28.0: resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==} - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@6.0.0: - resolution: {integrity: sha512-mkUH+rPkwU2qPadJ0oJZOjeZ5Mxn8Q1UhevwkTRWNuUZzyia3h4rhzK39hxaHTk0o2OxB8W2SQ6A8k23ZDi1pQ==} + nanoid@6.0.1: + resolution: {integrity: sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw==} engines: {node: ^22 || ^24 || >=26} hasBin: true @@ -9900,8 +9884,8 @@ packages: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} - nodemailer@9.0.3: - resolution: {integrity: sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==} + nodemailer@9.0.4: + resolution: {integrity: sha512-LmJNRVRtfSCULxcZpy0Cpg4WWenlUZ9+zbmTO+S7v9wD6XreYLjXRFtDjtV/4F0HT5p1GyZfA0Ux/myxHb18CQ==} engines: {node: '>=6.0.0'} nopt@1.0.10: @@ -9993,8 +9977,8 @@ packages: resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} engines: {node: '>=12.20.0'} - oidc-provider@9.10.0: - resolution: {integrity: sha512-Olmg6oxgHIviZnrf9yaey6vdaWG1f19UiyvpSj0gCgj6wl9YMcOw3uDv3FHj+x62EXeFj7GfZeTL09MCgIEKnw==} + oidc-provider@9.11.2: + resolution: {integrity: sha512-Z7CZzWtBfcNMSaIoNJwHmEbla6h4mW73soOAJM4Qo9XDALMgQ1J+amMg/FBZJD1BHoec1KLL5BHFDkvlgeUZqw==} on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -10247,8 +10231,8 @@ packages: peerDependencies: pg: '>=8.0' - pg-protocol@1.15.0: - resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + pg-protocol@1.16.0: + resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} @@ -10297,13 +10281,13 @@ packages: resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} engines: {node: '>=16.0.0'} - playwright-core@1.62.0: - resolution: {integrity: sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==} + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} engines: {node: '>=20'} hasBin: true - playwright@1.62.0: - resolution: {integrity: sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==} + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} engines: {node: '>=20'} hasBin: true @@ -10785,8 +10769,8 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss@8.5.25: - resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -11282,8 +11266,8 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rolldown@1.1.5: - resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + rolldown@1.2.3: + resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -11520,8 +11504,8 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - simple-icons@16.27.1: - resolution: {integrity: sha512-slZF8iKxkv7Lb9SF3L1AcIl5iYLNvDrKKm8yV69cG66XvxJ12SX+bYTgxRwCY4bXSmklGXoXaKEyVmEypOCeqw==} + simple-icons@16.28.0: + resolution: {integrity: sha512-sQPR5AtK/ijRjou7zw7mlLp08oB6FH7i0lOy5XJ2zp9mJs/yejgiOn7KvQoe2q4YJIx6VmgUSW5AOefebPt5kg==} engines: {node: '>=0.12.18'} sirv@2.0.4: @@ -11794,13 +11778,13 @@ packages: peerDependencies: svelte: '>= 3.43.1 < 6' - svelte-check@4.7.3: - resolution: {integrity: sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==} + svelte-check@4.7.4: + resolution: {integrity: sha512-IW9ot9YqAoyv8FvyN+eb4ZTe8zgcKZrJLNYU6dzSKkGwEBsSPc4K7lmQ8bKn8W2YMXM6WDfZSSVOaGtekyUfOQ==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: '>=5.0.0' + typescript: ^5.0.0 || ^6.0.0 svelte-eslint-parser@1.8.0: resolution: {integrity: sha512-mikR1qwIVy3t5WthUoAXkMwxkXvabZP9FJgdx35Ei7EbGWmctva1Pih16Koeor/bdNNq8NXHlwKGS6NkYTawLg==} @@ -11921,15 +11905,17 @@ packages: tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} - tailwind-variants@3.2.2: - resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} - engines: {node: '>=16.x', pnpm: '>=7.x'} + tailwind-variants@3.3.1: + resolution: {integrity: sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw==} + engines: {node: '>=16.9.x', pnpm: '>=7.x'} peerDependencies: tailwind-merge: '>=3.0.0' tailwindcss: '*' peerDependenciesMeta: tailwind-merge: optional: true + tailwindcss: + optional: true tailwindcss-email-variants@3.0.5: resolution: {integrity: sha512-BbULkY0/0VLXwSzAZP0wxyO0siXJARatreZTSW+U6dM+LuTzPsfG/SfxxuouINM1xtIoPGGU++jKkMyfYKJe7Q==} @@ -12033,8 +12019,9 @@ packages: engines: {node: '>=10'} hasBin: true - testcontainers@12.0.4: - resolution: {integrity: sha512-QIR/8xF1+F/26cIM+9B4yyxNTbKJxAv3hygZyhPRgZ8Q2AhlPZjDdpXRuk16V37X4bgJRI3hXFhoEICMBA7Adg==} + testcontainers@12.1.0: + resolution: {integrity: sha512-YjDLqIITuhGLMnM10yhg3oV6lIG5IMpz1R1DPBZoOOks83q7i7IVpeSWRTiyl7roozjiyLmwIoLK/KY8OnZmIA==} + engines: {node: '>= 22.22'} text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} @@ -12227,8 +12214,8 @@ packages: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} - tsx@4.23.1: - resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} + tsx@4.23.9: + resolution: {integrity: sha512-6q8uTORRGauQVjqMQnKUucLFoeXZAfw6zKvG35GLbdKWbLdeOtZ3H4mhyA5mxuUd2o2cRTskhj59nLLQseUvUw==} engines: {node: '>=18.0.0'} hasBin: true @@ -12283,8 +12270,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.65.0: - resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} + typescript-eslint@8.66.0: + resolution: {integrity: sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -12339,8 +12326,8 @@ packages: resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==} engines: {node: '>=18.17'} - undici@8.7.0: - resolution: {integrity: sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ==} + undici@8.10.0: + resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} engines: {node: '>=22.19.0'} unicode-canonical-property-names-ecmascript@2.0.1: @@ -12414,8 +12401,8 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin-swc@1.5.9: - resolution: {integrity: sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==} + unplugin-swc@1.5.10: + resolution: {integrity: sha512-aNvKH601KYzc/fXyDQJPAk8obNPzA4GdGqx+qSjQoyweMLDMLs5VRbunjL4skIv/hyyAnSgrbxMwXHdRTzhmew==} peerDependencies: '@swc/core': ^1.2.108 @@ -12580,13 +12567,13 @@ packages: yaml: optional: true - vite@8.1.5: - resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 + '@vitejs/devtools': ^0.4.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -14137,272 +14124,272 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-alpha-function@1.0.1(postcss@8.5.25)': + '@csstools/postcss-alpha-function@1.0.1(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.25)': + '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.26)': dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - '@csstools/postcss-color-function-display-p3-linear@1.0.1(postcss@8.5.25)': + '@csstools/postcss-color-function-display-p3-linear@1.0.1(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-color-function@4.0.12(postcss@8.5.25)': + '@csstools/postcss-color-function@4.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-color-mix-function@3.0.12(postcss@8.5.25)': + '@csstools/postcss-color-mix-function@3.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-color-mix-variadic-function-arguments@1.0.2(postcss@8.5.25)': + '@csstools/postcss-color-mix-variadic-function-arguments@1.0.2(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-content-alt-text@2.0.8(postcss@8.5.25)': + '@csstools/postcss-content-alt-text@2.0.8(postcss@8.5.26)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-contrast-color-function@2.0.12(postcss@8.5.25)': + '@csstools/postcss-contrast-color-function@2.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.25)': + '@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.26)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.25)': + '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.26)': dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.25)': + '@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-gradients-interpolation-method@5.0.12(postcss@8.5.25)': + '@csstools/postcss-gradients-interpolation-method@5.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-hwb-function@4.0.12(postcss@8.5.25)': + '@csstools/postcss-hwb-function@4.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-ic-unit@4.0.4(postcss@8.5.25)': + '@csstools/postcss-ic-unit@4.0.4(postcss@8.5.26)': dependencies: - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-initial@2.0.1(postcss@8.5.25)': + '@csstools/postcss-initial@2.0.1(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.25)': + '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.26)': dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - '@csstools/postcss-light-dark-function@2.0.11(postcss@8.5.25)': + '@csstools/postcss-light-dark-function@2.0.11(postcss@8.5.26)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.25)': + '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.25)': + '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.25)': + '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.25)': + '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.25)': + '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.26)': dependencies: '@csstools/css-tokenizer': 3.0.4 - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.25)': + '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.26)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.25)': + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.26)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.25)': + '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.26)': dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-normalize-display-values@4.0.1(postcss@8.5.25)': + '@csstools/postcss-normalize-display-values@4.0.1(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-oklab-function@4.0.12(postcss@8.5.25)': + '@csstools/postcss-oklab-function@4.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-position-area-property@1.0.0(postcss@8.5.25)': + '@csstools/postcss-position-area-property@1.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-progressive-custom-properties@4.2.1(postcss@8.5.25)': + '@csstools/postcss-progressive-custom-properties@4.2.1(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-property-rule-prelude-list@1.0.0(postcss@8.5.25)': + '@csstools/postcss-property-rule-prelude-list@1.0.0(postcss@8.5.26)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-random-function@2.0.1(postcss@8.5.25)': + '@csstools/postcss-random-function@2.0.1(postcss@8.5.26)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-relative-color-syntax@3.0.12(postcss@8.5.25)': + '@csstools/postcss-relative-color-syntax@3.0.12(postcss@8.5.26)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.25)': + '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.25)': + '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.26)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.25)': + '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.26)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-syntax-descriptor-syntax-production@1.0.1(postcss@8.5.25)': + '@csstools/postcss-syntax-descriptor-syntax-production@1.0.1(postcss@8.5.26)': dependencies: '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-system-ui-font-family@1.0.0(postcss@8.5.25)': + '@csstools/postcss-system-ui-font-family@1.0.0(postcss@8.5.26)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.25)': + '@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.26)': dependencies: '@csstools/color-helpers': 5.1.0 - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.25)': + '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.26)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 - '@csstools/postcss-unset-value@4.0.0(postcss@8.5.25)': + '@csstools/postcss-unset-value@4.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 '@csstools/selector-resolve-nested@3.1.0(postcss-selector-parser@7.1.4)': dependencies: @@ -14412,27 +14399,27 @@ snapshots: dependencies: postcss-selector-parser: 7.1.4 - '@csstools/utilities@2.0.0(postcss@8.5.25)': + '@csstools/utilities@2.0.0(postcss@8.5.26)': dependencies: - postcss: 8.5.25 + postcss: 8.5.26 '@discoveryjs/json-ext@0.5.7': {} - '@docsearch/core@4.6.3(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@docsearch/core@4.6.3(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) '@docsearch/css@4.6.3': {} - '@docsearch/react@4.6.3(@algolia/client-search@5.56.0)(@types/react@19.2.17)(algoliasearch@5.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)': + '@docsearch/react@4.6.3(@algolia/client-search@5.56.0)(@types/react@19.2.18)(algoliasearch@5.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)': dependencies: '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.56.0)(algoliasearch@5.56.0)(search-insights@2.17.3) - '@docsearch/core': 4.6.3(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@docsearch/core': 4.6.3(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@docsearch/css': 4.6.3 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) search-insights: 2.17.3 @@ -14440,7 +14427,7 @@ snapshots: - '@algolia/client-search' - algoliasearch - '@docusaurus/babel@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/babel@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/generator': 7.29.7 @@ -14452,7 +14439,7 @@ snapshots: '@babel/runtime': 7.29.7 '@babel/traverse': 7.29.7(supports-color@8.1.1) '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) babel-plugin-dynamic-import-node: 2.3.3 fs-extra: 11.3.6 tslib: 2.8.1 @@ -14474,32 +14461,32 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/bundler@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/bundler@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) - '@docusaurus/babel': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/babel': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/cssnano-preset': 3.10.2 '@docusaurus/logger': 3.10.2 - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - babel-loader: 9.2.1(@babel/core@7.29.7(supports-color@8.1.1))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + babel-loader: 9.2.1(@babel/core@7.29.7(supports-color@8.1.1))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) clean-css: 5.3.3 - copy-webpack-plugin: 11.0.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - css-loader: 6.11.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - cssnano: 6.1.2(postcss@8.5.25) - file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + copy-webpack-plugin: 11.0.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + css-loader: 6.11.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + cssnano: 6.1.2(postcss@8.5.26) + file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) html-minifier-terser: 7.2.0 - mini-css-extract-plugin: 2.10.2(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - null-loader: 4.0.1(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - postcss: 8.5.25 - postcss-loader: 7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.25)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - postcss-preset-env: 10.6.1(postcss@8.5.25) - terser-webpack-plugin: 5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + mini-css-extract-plugin: 2.10.2(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + null-loader: 4.0.1(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + postcss: 8.5.26 + postcss-loader: 7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.26)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + postcss-preset-env: 10.6.1(postcss@8.5.26) + terser-webpack-plugin: 5.6.1(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) - webpackbar: 7.0.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) + webpackbar: 7.0.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) transitivePeerDependencies: - '@minify-html/node' - '@parcel/css' @@ -14517,16 +14504,16 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/babel': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/bundler': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/babel': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/bundler': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.8) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@mdx-js/react': 3.1.1(@types/react@19.2.18)(react@19.2.8) boxen: 6.2.1 chalk: 4.1.2 chokidar: 3.6.0 @@ -14541,7 +14528,7 @@ snapshots: execa: 5.1.1 fs-extra: 11.3.6 html-tags: 3.3.1 - html-webpack-plugin: 5.6.7(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + html-webpack-plugin: 5.6.7(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) leven: 3.1.0 lodash: 4.18.1 open: 8.4.2 @@ -14551,7 +14538,7 @@ snapshots: react-dom: 19.2.8(react@19.2.8) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.8)' - react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.8))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.8))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) react-router: 5.3.4(react@19.2.8) react-router-config: 5.1.1(react-router@5.3.4(react@19.2.8))(react@19.2.8) react-router-dom: 5.3.4(react@19.2.8) @@ -14560,9 +14547,9 @@ snapshots: tinypool: 1.1.1 tslib: 2.8.1 update-notifier: 6.0.2 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + webpack-dev-server: 5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) webpack-merge: 6.0.1 transitivePeerDependencies: - '@minify-html/node' @@ -14588,9 +14575,9 @@ snapshots: '@docusaurus/cssnano-preset@3.10.2': dependencies: - cssnano-preset-advanced: 6.1.2(postcss@8.5.25) - postcss: 8.5.25 - postcss-sort-media-queries: 5.2.0(postcss@8.5.25) + cssnano-preset-advanced: 6.1.2(postcss@8.5.26) + postcss: 8.5.26 + postcss-sort-media-queries: 5.2.0(postcss@8.5.26) tslib: 2.8.1 '@docusaurus/logger@3.10.2': @@ -14598,16 +14585,16 @@ snapshots: chalk: 4.1.2 tslib: 2.8.1 - '@docusaurus/mdx-loader@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/mdx-loader@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@mdx-js/mdx': 3.1.1(supports-color@8.1.1) '@slorber/remark-comment': 1.0.0 escape-html: 1.0.3 estree-util-value-to-estree: 3.5.0 - file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) fs-extra: 11.3.6 image-size: 2.0.2 mdast-util-mdx: 3.0.0(supports-color@8.1.1) @@ -14623,9 +14610,9 @@ snapshots: tslib: 2.8.1 unified: 11.0.5 unist-util-visit: 5.1.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) vfile: 6.0.3 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -14642,11 +14629,11 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/module-type-aliases@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/module-type-aliases@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@types/history': 4.7.11 - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@types/react-router-config': 5.0.11 '@types/react-router-dom': 5.3.3 react: 19.2.8 @@ -14669,17 +14656,17 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-content-blog@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) cheerio: 1.0.0-rc.12 combine-promises: 1.2.0 feed: 4.2.2 @@ -14692,7 +14679,7 @@ snapshots: tslib: 2.8.1 unist-util-visit: 5.1.0 utility-types: 3.11.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14717,17 +14704,17 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 fs-extra: 11.3.6 @@ -14738,7 +14725,7 @@ snapshots: schema-dts: 1.1.5 tslib: 2.8.1 utility-types: 3.11.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14763,18 +14750,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-content-pages@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) fs-extra: 11.3.6 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14799,12 +14786,12 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-css-cascade-layers@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -14832,11 +14819,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-debug@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) fs-extra: 11.3.6 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -14866,11 +14853,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-google-analytics@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 @@ -14898,11 +14885,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-google-gtag@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 @@ -14930,11 +14917,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-google-tag-manager@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 @@ -14962,14 +14949,14 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-sitemap@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) fs-extra: 11.3.6 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -14999,18 +14986,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/plugin-svgr@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@svgr/webpack': 8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -15035,23 +15022,23 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/preset-classic@3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-blog': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-css-cascade-layers': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-debug': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-google-analytics': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-google-gtag': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-google-tag-manager': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-sitemap': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-svgr': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-classic': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-search-algolia': 3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-blog': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-css-cascade-layers': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-debug': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-google-analytics': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-google-gtag': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-google-tag-manager': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-sitemap': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-svgr': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-classic': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-search-algolia': 3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) transitivePeerDependencies: @@ -15083,31 +15070,31 @@ snapshots: '@docusaurus/react-loadable@6.0.0(react@19.2.8)': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 react: 19.2.8 - '@docusaurus/theme-classic@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/theme-classic@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-blog': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-blog': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/theme-translations': 3.10.2 - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.8) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@mdx-js/react': 3.1.1(@types/react@19.2.18)(react@19.2.8) clsx: 2.1.1 copy-text-to-clipboard: 3.2.2 infima: 0.2.0-alpha.45 lodash: 4.18.1 nprogress: 0.2.0 - postcss: 8.5.25 + postcss: 8.5.26 prism-react-renderer: 2.4.1(react@19.2.8) prismjs: 1.30.0 react: 19.2.8 @@ -15139,15 +15126,15 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/theme-common@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@types/history': 4.7.11 - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@types/react-router-config': 5.0.11 clsx: 2.1.1 parse-numeric-range: 1.3.0 @@ -15172,13 +15159,13 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-mermaid@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/theme-mermaid@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) mermaid: 11.16.0 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -15208,17 +15195,17 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-search-algolia@3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/theme-search-algolia@3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/react@19.2.18)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@algolia/autocomplete-core': 1.19.9(@algolia/client-search@5.56.0)(algoliasearch@5.56.0)(search-insights@2.17.3) - '@docsearch/react': 4.6.3(@algolia/client-search@5.56.0)(@types/react@19.2.17)(algoliasearch@5.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3) - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docsearch/react': 4.6.3(@algolia/client-search@5.56.0)(@types/react@19.2.18)(algoliasearch@5.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/theme-translations': 3.10.2 - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) algoliasearch: 5.56.0 algoliasearch-helper: 3.29.2(algoliasearch@5.56.0) clsx: 2.1.1 @@ -15263,19 +15250,19 @@ snapshots: '@docusaurus/tsconfig@3.10.2': {} - '@docusaurus/types@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/types@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@mdx-js/mdx': 3.1.1(supports-color@8.1.1) '@types/history': 4.7.11 '@types/mdast': 4.0.4 - '@types/react': 19.2.17 + '@types/react': 19.2.18 commander: 5.1.0 joi: 17.13.4 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' utility-types: 3.11.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) webpack-merge: 5.10.0 transitivePeerDependencies: - '@minify-html/node' @@ -15293,9 +15280,9 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-common@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/utils-common@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) tslib: 2.8.1 transitivePeerDependencies: - '@minify-html/node' @@ -15315,11 +15302,11 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-validation@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/utils-validation@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) fs-extra: 11.3.6 joi: 17.13.4 js-yaml: 4.3.0 @@ -15343,15 +15330,15 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': + '@docusaurus/utils@3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@11ty/gray-matter': 1.0.0 '@docusaurus/logger': 3.10.2 - '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) - '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) escape-string-regexp: 4.0.0 execa: 5.1.1 - file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) fs-extra: 11.3.6 github-slugger: 1.5.0 globby: 11.1.0 @@ -15363,9 +15350,9 @@ snapshots: prompts: 2.4.2 resolve-pathname: 3.0.0 tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) utility-types: 3.11.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -15384,29 +15371,18 @@ snapshots: - uglify-js - webpack-cli - '@emnapi/core@1.11.1': - dependencies: - '@emnapi/wasi-threads': 1.2.2 - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.2': - dependencies: - tslib: 2.8.1 - optional: true - '@esbuild/aix-ppc64@0.19.12': optional: true '@esbuild/aix-ppc64@0.27.4': optional: true - '@esbuild/aix-ppc64@0.28.1': + '@esbuild/aix-ppc64@0.28.2': optional: true '@esbuild/android-arm64@0.19.12': @@ -15415,7 +15391,7 @@ snapshots: '@esbuild/android-arm64@0.27.4': optional: true - '@esbuild/android-arm64@0.28.1': + '@esbuild/android-arm64@0.28.2': optional: true '@esbuild/android-arm@0.19.12': @@ -15424,7 +15400,7 @@ snapshots: '@esbuild/android-arm@0.27.4': optional: true - '@esbuild/android-arm@0.28.1': + '@esbuild/android-arm@0.28.2': optional: true '@esbuild/android-x64@0.19.12': @@ -15433,7 +15409,7 @@ snapshots: '@esbuild/android-x64@0.27.4': optional: true - '@esbuild/android-x64@0.28.1': + '@esbuild/android-x64@0.28.2': optional: true '@esbuild/darwin-arm64@0.19.12': @@ -15442,7 +15418,7 @@ snapshots: '@esbuild/darwin-arm64@0.27.4': optional: true - '@esbuild/darwin-arm64@0.28.1': + '@esbuild/darwin-arm64@0.28.2': optional: true '@esbuild/darwin-x64@0.19.12': @@ -15451,7 +15427,7 @@ snapshots: '@esbuild/darwin-x64@0.27.4': optional: true - '@esbuild/darwin-x64@0.28.1': + '@esbuild/darwin-x64@0.28.2': optional: true '@esbuild/freebsd-arm64@0.19.12': @@ -15460,7 +15436,7 @@ snapshots: '@esbuild/freebsd-arm64@0.27.4': optional: true - '@esbuild/freebsd-arm64@0.28.1': + '@esbuild/freebsd-arm64@0.28.2': optional: true '@esbuild/freebsd-x64@0.19.12': @@ -15469,7 +15445,7 @@ snapshots: '@esbuild/freebsd-x64@0.27.4': optional: true - '@esbuild/freebsd-x64@0.28.1': + '@esbuild/freebsd-x64@0.28.2': optional: true '@esbuild/linux-arm64@0.19.12': @@ -15478,7 +15454,7 @@ snapshots: '@esbuild/linux-arm64@0.27.4': optional: true - '@esbuild/linux-arm64@0.28.1': + '@esbuild/linux-arm64@0.28.2': optional: true '@esbuild/linux-arm@0.19.12': @@ -15487,7 +15463,7 @@ snapshots: '@esbuild/linux-arm@0.27.4': optional: true - '@esbuild/linux-arm@0.28.1': + '@esbuild/linux-arm@0.28.2': optional: true '@esbuild/linux-ia32@0.19.12': @@ -15496,7 +15472,7 @@ snapshots: '@esbuild/linux-ia32@0.27.4': optional: true - '@esbuild/linux-ia32@0.28.1': + '@esbuild/linux-ia32@0.28.2': optional: true '@esbuild/linux-loong64@0.19.12': @@ -15505,7 +15481,7 @@ snapshots: '@esbuild/linux-loong64@0.27.4': optional: true - '@esbuild/linux-loong64@0.28.1': + '@esbuild/linux-loong64@0.28.2': optional: true '@esbuild/linux-mips64el@0.19.12': @@ -15514,7 +15490,7 @@ snapshots: '@esbuild/linux-mips64el@0.27.4': optional: true - '@esbuild/linux-mips64el@0.28.1': + '@esbuild/linux-mips64el@0.28.2': optional: true '@esbuild/linux-ppc64@0.19.12': @@ -15523,7 +15499,7 @@ snapshots: '@esbuild/linux-ppc64@0.27.4': optional: true - '@esbuild/linux-ppc64@0.28.1': + '@esbuild/linux-ppc64@0.28.2': optional: true '@esbuild/linux-riscv64@0.19.12': @@ -15532,7 +15508,7 @@ snapshots: '@esbuild/linux-riscv64@0.27.4': optional: true - '@esbuild/linux-riscv64@0.28.1': + '@esbuild/linux-riscv64@0.28.2': optional: true '@esbuild/linux-s390x@0.19.12': @@ -15541,7 +15517,7 @@ snapshots: '@esbuild/linux-s390x@0.27.4': optional: true - '@esbuild/linux-s390x@0.28.1': + '@esbuild/linux-s390x@0.28.2': optional: true '@esbuild/linux-x64@0.19.12': @@ -15550,13 +15526,13 @@ snapshots: '@esbuild/linux-x64@0.27.4': optional: true - '@esbuild/linux-x64@0.28.1': + '@esbuild/linux-x64@0.28.2': optional: true '@esbuild/netbsd-arm64@0.27.4': optional: true - '@esbuild/netbsd-arm64@0.28.1': + '@esbuild/netbsd-arm64@0.28.2': optional: true '@esbuild/netbsd-x64@0.19.12': @@ -15565,13 +15541,13 @@ snapshots: '@esbuild/netbsd-x64@0.27.4': optional: true - '@esbuild/netbsd-x64@0.28.1': + '@esbuild/netbsd-x64@0.28.2': optional: true '@esbuild/openbsd-arm64@0.27.4': optional: true - '@esbuild/openbsd-arm64@0.28.1': + '@esbuild/openbsd-arm64@0.28.2': optional: true '@esbuild/openbsd-x64@0.19.12': @@ -15580,13 +15556,13 @@ snapshots: '@esbuild/openbsd-x64@0.27.4': optional: true - '@esbuild/openbsd-x64@0.28.1': + '@esbuild/openbsd-x64@0.28.2': optional: true '@esbuild/openharmony-arm64@0.27.4': optional: true - '@esbuild/openharmony-arm64@0.28.1': + '@esbuild/openharmony-arm64@0.28.2': optional: true '@esbuild/sunos-x64@0.19.12': @@ -15595,7 +15571,7 @@ snapshots: '@esbuild/sunos-x64@0.27.4': optional: true - '@esbuild/sunos-x64@0.28.1': + '@esbuild/sunos-x64@0.28.2': optional: true '@esbuild/win32-arm64@0.19.12': @@ -15604,7 +15580,7 @@ snapshots: '@esbuild/win32-arm64@0.27.4': optional: true - '@esbuild/win32-arm64@0.28.1': + '@esbuild/win32-arm64@0.28.2': optional: true '@esbuild/win32-ia32@0.19.12': @@ -15613,7 +15589,7 @@ snapshots: '@esbuild/win32-ia32@0.27.4': optional: true - '@esbuild/win32-ia32@0.28.1': + '@esbuild/win32-ia32@0.28.2': optional: true '@esbuild/win32-x64@0.19.12': @@ -15622,7 +15598,7 @@ snapshots: '@esbuild/win32-x64@0.27.4': optional: true - '@esbuild/win32-x64@0.28.1': + '@esbuild/win32-x64@0.28.2': optional: true '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))': @@ -15707,7 +15683,7 @@ snapshots: '@formatjs/icu-skeleton-parser': 1.8.16 tslib: 2.8.1 - '@formatjs/icu-messageformat-parser@3.5.15': + '@formatjs/icu-messageformat-parser@3.5.16': dependencies: '@formatjs/icu-skeleton-parser': 2.1.11 @@ -15894,18 +15870,19 @@ snapshots: pg-connection-string: 2.14.0 postgres: 3.4.9 - '@immich/ui@0.83.0(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))': + '@immich/ui@0.85.0(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))': dependencies: '@internationalized/date': 3.12.2 '@mdi/js': 7.4.47 - '@sveltejs/kit': 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) - bits-ui: 2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + '@sveltejs/kit': 2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) + bits-ui: 2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + fuse.js: 7.5.0 luxon: 3.7.2 - simple-icons: 16.27.1 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + simple-icons: 16.28.0 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) svelte-highlight: 7.11.0 tailwind-merge: 3.6.0 - tailwind-variants: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3) + tailwind-variants: 3.3.1(tailwind-merge@3.6.0)(tailwindcss@4.3.3) tailwindcss: 4.3.3 '@inquirer/ansi@1.0.2': {} @@ -16261,8 +16238,8 @@ snapshots: '@koddsson/eslint-plugin-tscompat@0.2.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@mdn/browser-compat-data': 6.1.5 - '@typescript-eslint/type-utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/type-utils': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/utils': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) browserslist: 4.28.6 transitivePeerDependencies: - eslint @@ -16395,10 +16372,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8)': + '@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8)': dependencies: '@types/mdx': 2.0.14 - '@types/react': 19.2.17 + '@types/react': 19.2.18 react: 19.2.8 '@mermaid-js/parser@1.2.0': @@ -16427,28 +16404,21 @@ snapshots: '@namnode/store@0.1.0': {} - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 - optional: true - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)': dependencies: '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(bullmq@5.81.2(supports-color@8.1.1))': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(bullmq@5.81.3(supports-color@8.1.1))': dependencies: '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28) '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) - bullmq: 5.81.2(supports-color@8.1.1) + bullmq: 5.81.3(supports-color@8.1.1) tslib: 2.8.1 - '@nestjs/cli@11.0.24(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/node@24.13.3)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(prettier@3.9.6)(uglify-js@3.19.3)': + '@nestjs/cli@11.0.24(@swc/core@1.15.47(@swc/helpers@0.5.23))(@types/node@24.13.3)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(prettier@3.9.6)(uglify-js@3.19.3)': dependencies: '@angular-devkit/core': 19.2.27(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) @@ -16459,17 +16429,17 @@ snapshots: chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) glob: 13.0.6 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) webpack-node-externals: 3.0.0 optionalDependencies: - '@swc/core': 1.15.46(@swc/helpers@0.5.23) + '@swc/core': 1.15.47(@swc/helpers@0.5.23) transitivePeerDependencies: - '@minify-html/node' - '@swc/css' @@ -16610,7 +16580,7 @@ snapshots: '@noble/hashes@1.8.0': {} - '@noble/hashes@2.2.0': {} + '@noble/hashes@2.3.0': {} '@nodelib/fs.scandir@2.1.5': dependencies: @@ -16901,7 +16871,7 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) - '@oxc-project/types@0.139.0': {} + '@oxc-project/types@0.143.0': {} '@paralleldrive/cuid2@2.3.1': dependencies: @@ -17062,32 +17032,32 @@ snapshots: tslib: 2.8.1 tsyringe: 4.10.0 - '@photo-sphere-viewer/core@5.15.0': + '@photo-sphere-viewer/core@5.15.1': dependencies: three: 0.185.1 - '@photo-sphere-viewer/equirectangular-video-adapter@5.15.0(@photo-sphere-viewer/core@5.15.0)(@photo-sphere-viewer/video-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0))': + '@photo-sphere-viewer/equirectangular-video-adapter@5.15.1(@photo-sphere-viewer/core@5.15.1)(@photo-sphere-viewer/video-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1))': dependencies: - '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/video-plugin': 5.15.0(@photo-sphere-viewer/core@5.15.0) + '@photo-sphere-viewer/core': 5.15.1 + '@photo-sphere-viewer/video-plugin': 5.15.1(@photo-sphere-viewer/core@5.15.1) three: 0.185.1 - '@photo-sphere-viewer/markers-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)': + '@photo-sphere-viewer/markers-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1)': dependencies: - '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 - '@photo-sphere-viewer/resolution-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)(@photo-sphere-viewer/settings-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0))': + '@photo-sphere-viewer/resolution-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1)(@photo-sphere-viewer/settings-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1))': dependencies: - '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/settings-plugin': 5.15.0(@photo-sphere-viewer/core@5.15.0) + '@photo-sphere-viewer/core': 5.15.1 + '@photo-sphere-viewer/settings-plugin': 5.15.1(@photo-sphere-viewer/core@5.15.1) - '@photo-sphere-viewer/settings-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)': + '@photo-sphere-viewer/settings-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1)': dependencies: - '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 - '@photo-sphere-viewer/video-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)': + '@photo-sphere-viewer/video-plugin@5.15.1(@photo-sphere-viewer/core@5.15.1)': dependencies: - '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/core': 5.15.1 three: 0.185.1 '@photostructure/tz-lookup@11.5.0': {} @@ -17097,9 +17067,9 @@ snapshots: '@pkgr/core@0.3.6': {} - '@playwright/test@1.62.0': + '@playwright/test@1.62.1': dependencies: - playwright: 1.62.0 + playwright: 1.62.1 '@pnpm/config.env-replace@1.1.0': {} @@ -17274,53 +17244,46 @@ snapshots: '@codemirror/state': 6.7.1 '@codemirror/view': 6.43.7 - '@rolldown/binding-android-arm64@1.1.5': + '@rolldown/binding-android-arm64@1.2.3': optional: true - '@rolldown/binding-darwin-arm64@1.1.5': + '@rolldown/binding-darwin-arm64@1.2.3': optional: true - '@rolldown/binding-darwin-x64@1.1.5': + '@rolldown/binding-darwin-x64@1.2.3': optional: true - '@rolldown/binding-freebsd-x64@1.1.5': + '@rolldown/binding-freebsd-x64@1.2.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.5': + '@rolldown/binding-linux-arm64-gnu@1.2.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.5': + '@rolldown/binding-linux-arm64-musl@1.2.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.5': + '@rolldown/binding-linux-ppc64-gnu@1.2.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.5': + '@rolldown/binding-linux-s390x-gnu@1.2.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.5': + '@rolldown/binding-linux-x64-gnu@1.2.3': optional: true - '@rolldown/binding-linux-x64-musl@1.1.5': + '@rolldown/binding-linux-x64-musl@1.2.3': optional: true - '@rolldown/binding-openharmony-arm64@1.1.5': + '@rolldown/binding-openharmony-arm64@1.2.3': optional: true - '@rolldown/binding-wasm32-wasi@1.1.5': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@rolldown/binding-win32-arm64-msvc@1.2.3': optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.5': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.1.5': + '@rolldown/binding-win32-x64-msvc@1.2.3': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -17460,58 +17423,58 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.11(acorn@8.18.0)': + '@sveltejs/acorn-typescript@1.0.12(acorn@8.18.0)': dependencies: acorn: 8.18.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))': dependencies: - '@sveltejs/kit': 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@sveltejs/kit': 2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) - '@sveltejs/enhanced-img@0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@sveltejs/enhanced-img@0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) magic-string: 0.30.21 sharp: 0.34.5 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) - svelte-parse-markup: 0.1.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + svelte-parse-markup: 0.1.5(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) vite-imagetools: 9.0.3(rollup@4.62.0) zimmerframe: 1.1.4 transitivePeerDependencies: - rollup - '@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.11(acorn@8.18.0) - '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@sveltejs/acorn-typescript': 1.0.12(acorn@8.18.0) + '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@types/cookie': 0.6.0 acorn: 8.18.0 cookie: 0.6.0 - devalue: 5.8.2 + devalue: 5.9.0 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 mrmime: 2.0.1 set-cookie-parser: 3.1.2 sirv: 3.0.2 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) optionalDependencies: '@opentelemetry/api': 1.9.1 typescript: '@typescript/typescript6@6.0.2' - '@sveltejs/load-config@0.2.0': {} + '@sveltejs/load-config@0.2.2': {} - '@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.3 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) - vitefu: 1.1.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: @@ -17606,59 +17569,59 @@ snapshots: - supports-color - typescript - '@swc/core-darwin-arm64@1.15.46': + '@swc/core-darwin-arm64@1.15.47': optional: true - '@swc/core-darwin-x64@1.15.46': + '@swc/core-darwin-x64@1.15.47': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.46': + '@swc/core-linux-arm-gnueabihf@1.15.47': optional: true - '@swc/core-linux-arm64-gnu@1.15.46': + '@swc/core-linux-arm64-gnu@1.15.47': optional: true - '@swc/core-linux-arm64-musl@1.15.46': + '@swc/core-linux-arm64-musl@1.15.47': optional: true - '@swc/core-linux-ppc64-gnu@1.15.46': + '@swc/core-linux-ppc64-gnu@1.15.47': optional: true - '@swc/core-linux-s390x-gnu@1.15.46': + '@swc/core-linux-s390x-gnu@1.15.47': optional: true - '@swc/core-linux-x64-gnu@1.15.46': + '@swc/core-linux-x64-gnu@1.15.47': optional: true - '@swc/core-linux-x64-musl@1.15.46': + '@swc/core-linux-x64-musl@1.15.47': optional: true - '@swc/core-win32-arm64-msvc@1.15.46': + '@swc/core-win32-arm64-msvc@1.15.47': optional: true - '@swc/core-win32-ia32-msvc@1.15.46': + '@swc/core-win32-ia32-msvc@1.15.47': optional: true - '@swc/core-win32-x64-msvc@1.15.46': + '@swc/core-win32-x64-msvc@1.15.47': optional: true - '@swc/core@1.15.46(@swc/helpers@0.5.23)': + '@swc/core@1.15.47(@swc/helpers@0.5.23)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.27 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.46 - '@swc/core-darwin-x64': 1.15.46 - '@swc/core-linux-arm-gnueabihf': 1.15.46 - '@swc/core-linux-arm64-gnu': 1.15.46 - '@swc/core-linux-arm64-musl': 1.15.46 - '@swc/core-linux-ppc64-gnu': 1.15.46 - '@swc/core-linux-s390x-gnu': 1.15.46 - '@swc/core-linux-x64-gnu': 1.15.46 - '@swc/core-linux-x64-musl': 1.15.46 - '@swc/core-win32-arm64-msvc': 1.15.46 - '@swc/core-win32-ia32-msvc': 1.15.46 - '@swc/core-win32-x64-msvc': 1.15.46 + '@swc/core-darwin-arm64': 1.15.47 + '@swc/core-darwin-x64': 1.15.47 + '@swc/core-linux-arm-gnueabihf': 1.15.47 + '@swc/core-linux-arm64-gnu': 1.15.47 + '@swc/core-linux-arm64-musl': 1.15.47 + '@swc/core-linux-ppc64-gnu': 1.15.47 + '@swc/core-linux-s390x-gnu': 1.15.47 + '@swc/core-linux-x64-gnu': 1.15.47 + '@swc/core-linux-x64-musl': 1.15.47 + '@swc/core-win32-arm64-msvc': 1.15.47 + '@swc/core-win32-ia32-msvc': 1.15.47 + '@swc/core-win32-x64-msvc': 1.15.47 '@swc/helpers': 0.5.23 '@swc/counter@0.1.3': {} @@ -17736,12 +17699,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.3(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) '@testing-library/dom@10.4.1': dependencies: @@ -17764,20 +17727,20 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/svelte-core@1.1.3(svelte@5.56.8(@typescript-eslint/types@8.65.0))': + '@testing-library/svelte-core@1.1.3(svelte@5.56.8(@typescript-eslint/types@8.66.0))': dependencies: - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - '@testing-library/svelte@5.4.2(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))(vitest@4.1.10)': + '@testing-library/svelte@5.4.2(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@testing-library/dom': 10.4.1 - '@testing-library/svelte-core': 1.1.3(svelte@5.56.8(@typescript-eslint/types@8.65.0)) - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + '@testing-library/svelte-core': 1.1.3(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) - '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + '@testing-library/user-event@14.6.3(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 @@ -17790,7 +17753,7 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)))(prettier@3.9.6)(supports-color@8.1.1)(svelte@5.56.8(@typescript-eslint/types@8.65.0))': + '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)))(prettier@3.9.6)(supports-color@8.1.1)(svelte@5.56.8(@typescript-eslint/types@8.66.0))': dependencies: '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 @@ -17802,8 +17765,8 @@ snapshots: parse-imports-exports: 0.2.4 prettier: 3.9.6 optionalDependencies: - prettier-plugin-svelte: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + prettier-plugin-svelte: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) transitivePeerDependencies: - supports-color @@ -17826,11 +17789,6 @@ snapshots: '@types/geojson': 7946.0.16 tslib: 2.8.1 - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 - optional: true - '@types/accepts@1.3.7': dependencies: '@types/node': 24.13.3 @@ -18179,11 +18137,11 @@ snapshots: '@types/lodash-es@4.17.12': dependencies: - '@types/lodash': 4.17.24 + '@types/lodash': 4.17.25 - '@types/lodash@4.17.24': {} + '@types/lodash@4.17.25': {} - '@types/luxon@3.7.2': {} + '@types/luxon@3.7.3': {} '@types/mdast@4.0.4': dependencies: @@ -18223,7 +18181,7 @@ snapshots: dependencies: '@types/node': 24.13.3 - '@types/oidc-provider@9.5.0': + '@types/oidc-provider@9.11.0': dependencies: '@types/keygrip': 1.0.6 '@types/koa': 3.0.3 @@ -18233,18 +18191,18 @@ snapshots: '@types/pg-pool@2.0.7': dependencies: - '@types/pg': 8.20.0 + '@types/pg': 8.20.4 '@types/pg@8.15.6': dependencies: '@types/node': 24.13.3 - pg-protocol: 1.15.0 + pg-protocol: 1.16.0 pg-types: 2.2.0 - '@types/pg@8.20.0': + '@types/pg@8.20.4': dependencies: '@types/node': 24.13.3 - pg-protocol: 1.15.0 + pg-protocol: 1.16.0 pg-types: 2.2.0 '@types/picomatch@4.0.3': {} @@ -18266,21 +18224,21 @@ snapshots: '@types/react-router-config@5.0.11': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@types/react-router': 5.1.20 '@types/react-router-dom@5.3.3': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@types/react-router': 5.1.20 '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@types/react@19.2.17': + '@types/react@19.2.18': dependencies: csstype: 3.2.3 @@ -18294,7 +18252,7 @@ snapshots: dependencies: '@types/node': 24.13.3 - '@types/semver@7.7.1': {} + '@types/semver@7.8.0': {} '@types/send@0.17.6': dependencies: @@ -18375,14 +18333,14 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': + '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/type-utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/parser': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/type-utils': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/utils': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/visitor-keys': 8.66.0 eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) ignore: 7.0.6 natural-compare: 1.4.0 @@ -18391,41 +18349,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': + '@typescript-eslint/parser@8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/visitor-keys': 8.66.0 debug: 4.4.3(supports-color@8.1.1) eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': + '@typescript-eslint/project-service@8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.65.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/tsconfig-utils': 8.66.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.66.0 debug: 4.4.3(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.65.0': + '@typescript-eslint/scope-manager@8.66.0': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 - '@typescript-eslint/tsconfig-utils@8.65.0(@typescript/typescript6@6.0.2)': + '@typescript-eslint/tsconfig-utils@8.66.0(@typescript/typescript6@6.0.2)': dependencies: typescript: '@typescript/typescript6@6.0.2' - '@typescript-eslint/type-utils@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': + '@typescript-eslint/type-utils@8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/utils': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1) eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) @@ -18433,14 +18391,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.65.0': {} + '@typescript-eslint/types@8.66.0': {} - '@typescript-eslint/typescript-estree@8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': + '@typescript-eslint/typescript-estree@8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@typescript-eslint/project-service': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@typescript-eslint/tsconfig-utils': 8.65.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/visitor-keys': 8.65.0 + '@typescript-eslint/project-service': 8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/tsconfig-utils': 8.66.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.6 semver: 7.8.5 @@ -18450,20 +18408,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': + '@typescript-eslint/utils@8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) - '@typescript-eslint/scope-manager': 8.65.0 - '@typescript-eslint/types': 8.65.0 - '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.65.0': + '@typescript-eslint/visitor-keys@8.66.0': dependencies: - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/types': 8.66.0 eslint-visitor-keys: 5.0.1 '@typescript/typescript-aix-ppc64@7.0.2': @@ -18541,7 +18499,7 @@ snapshots: dependencies: valibot: 1.4.2(@typescript/typescript6@6.0.2) - '@vitest/coverage-v8@4.1.10(vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@vitest/coverage-v8@4.1.10(vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.10 @@ -18553,7 +18511,7 @@ snapshots: obug: 2.1.3 std-env: 4.2.0 tinyrainbow: 3.1.0 - vitest: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vitest: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': dependencies: @@ -18567,7 +18525,7 @@ snapshots: obug: 2.1.3 std-env: 4.2.0 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@vitest/expect@3.2.7': dependencies: @@ -18586,21 +18544,21 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) - '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) '@vitest/pretty-format@3.2.7': dependencies: @@ -18736,10 +18694,10 @@ snapshots: dependencies: '@namnode/store': 0.1.0 - '@zoom-image/svelte@0.3.9(svelte@5.56.8(@typescript-eslint/types@8.65.0))': + '@zoom-image/svelte@0.3.9(svelte@5.56.8(@typescript-eslint/types@8.66.0))': dependencies: '@zoom-image/core': 0.42.0 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) abbrev@1.1.1: {} @@ -18986,25 +18944,25 @@ snapshots: dependencies: immediate: 3.3.0 - autoprefixer@10.5.4(postcss@8.5.25): + autoprefixer@10.5.4(postcss@8.5.26): dependencies: browserslist: 4.28.6 caniuse-lite: 1.0.30001806 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 axobject-query@4.1.0: {} b4a@1.8.1: {} - babel-loader@9.2.1(@babel/core@7.29.7(supports-color@8.1.1))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + babel-loader@9.2.1(@babel/core@7.29.7(supports-color@8.1.1))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@babel/core': 7.29.7(supports-color@8.1.1) find-cache-dir: 4.0.0 schema-utils: 4.3.3 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) babel-plugin-dynamic-import-node@2.3.3: dependencies: @@ -19052,12 +19010,12 @@ snapshots: bare-events@2.9.1: {} - bare-fs@4.7.4: + bare-fs@4.8.0: dependencies: bare-events: 2.9.1 bare-path: 3.1.1 bare-stream: 2.13.3(bare-events@2.9.1) - bare-url: 2.4.5 + bare-url: 2.5.1 fast-fifo: 1.3.2 transitivePeerDependencies: - bare-abort-controller @@ -19075,7 +19033,7 @@ snapshots: transitivePeerDependencies: - react-native-b4a - bare-url@2.4.5: + bare-url@2.5.1: dependencies: bare-path: 3.1.1 @@ -19105,15 +19063,15 @@ snapshots: binary-extensions@2.3.0: {} - bits-ui@2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + bits-ui@2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: '@floating-ui/core': 1.8.0 '@floating-ui/dom': 1.8.0 '@internationalized/date': 3.12.2 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) - svelte: 5.56.8(@typescript-eslint/types@8.65.0) - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + runed: 0.35.1(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) tabbable: 6.5.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -19234,11 +19192,11 @@ snapshots: builtin-modules@5.2.0: {} - bullmq@5.81.2(supports-color@8.1.1): + bullmq@5.81.3(supports-color@8.1.1): dependencies: cron-parser: 4.9.0 ioredis: 5.11.1(supports-color@8.1.1) - msgpackr: 2.0.4 + msgpackr: 2.0.5 node-abort-controller: 3.1.1 semver: 7.8.5 tslib: 2.8.1 @@ -19657,7 +19615,7 @@ snapshots: copy-text-to-clipboard@3.2.2: {} - copy-webpack-plugin@11.0.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + copy-webpack-plugin@11.0.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -19665,7 +19623,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) core-js-compat@3.49.0: dependencies: @@ -19727,7 +19685,7 @@ snapshots: cron@4.4.0: dependencies: - '@types/luxon': 3.7.2 + '@types/luxon': 3.7.3 luxon: 3.7.2 cross-spawn@7.0.6: @@ -19740,53 +19698,53 @@ snapshots: dependencies: type-fest: 1.4.0 - css-blank-pseudo@7.0.1(postcss@8.5.25): + css-blank-pseudo@7.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - css-declaration-sorter@7.4.0(postcss@8.5.25): + css-declaration-sorter@7.4.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - css-has-pseudo@7.0.3(postcss@8.5.25): + css-has-pseudo@7.0.3(postcss@8.5.26): dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 postcss-value-parser: 4.2.0 - css-loader@6.11.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + css-loader@6.11.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: - icss-utils: 5.1.0(postcss@8.5.25) - postcss: 8.5.25 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.25) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.25) - postcss-modules-scope: 3.2.1(postcss@8.5.25) - postcss-modules-values: 4.0.0(postcss@8.5.25) + icss-utils: 5.1.0(postcss@8.5.26) + postcss: 8.5.26 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.26) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.26) + postcss-modules-scope: 3.2.1(postcss@8.5.26) + postcss-modules-values: 4.0.0(postcss@8.5.26) postcss-value-parser: 4.2.0 semver: 7.8.5 optionalDependencies: - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) - css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.2)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 - cssnano: 6.1.2(postcss@8.5.25) + cssnano: 6.1.2(postcss@8.5.26) jest-worker: 29.7.0 - postcss: 8.5.25 + postcss: 8.5.26 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) optionalDependencies: clean-css: 5.3.3 csso: 5.0.5 - esbuild: 0.28.1 + esbuild: 0.28.2 lightningcss: 1.33.0 - css-prefers-color-scheme@10.0.0(postcss@8.5.25): + css-prefers-color-scheme@10.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 css-select@4.3.0: dependencies: @@ -19824,60 +19782,60 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-advanced@6.1.2(postcss@8.5.25): + cssnano-preset-advanced@6.1.2(postcss@8.5.26): dependencies: - autoprefixer: 10.5.4(postcss@8.5.25) + autoprefixer: 10.5.4(postcss@8.5.26) browserslist: 4.28.6 - cssnano-preset-default: 6.1.2(postcss@8.5.25) - postcss: 8.5.25 - postcss-discard-unused: 6.0.5(postcss@8.5.25) - postcss-merge-idents: 6.0.3(postcss@8.5.25) - postcss-reduce-idents: 6.0.3(postcss@8.5.25) - postcss-zindex: 6.0.2(postcss@8.5.25) + cssnano-preset-default: 6.1.2(postcss@8.5.26) + postcss: 8.5.26 + postcss-discard-unused: 6.0.5(postcss@8.5.26) + postcss-merge-idents: 6.0.3(postcss@8.5.26) + postcss-reduce-idents: 6.0.3(postcss@8.5.26) + postcss-zindex: 6.0.2(postcss@8.5.26) - cssnano-preset-default@6.1.2(postcss@8.5.25): + cssnano-preset-default@6.1.2(postcss@8.5.26): dependencies: browserslist: 4.28.6 - css-declaration-sorter: 7.4.0(postcss@8.5.25) - cssnano-utils: 4.0.2(postcss@8.5.25) - postcss: 8.5.25 - postcss-calc: 9.0.1(postcss@8.5.25) - postcss-colormin: 6.1.0(postcss@8.5.25) - postcss-convert-values: 6.1.0(postcss@8.5.25) - postcss-discard-comments: 6.0.2(postcss@8.5.25) - postcss-discard-duplicates: 6.0.3(postcss@8.5.25) - postcss-discard-empty: 6.0.3(postcss@8.5.25) - postcss-discard-overridden: 6.0.2(postcss@8.5.25) - postcss-merge-longhand: 6.0.5(postcss@8.5.25) - postcss-merge-rules: 6.1.1(postcss@8.5.25) - postcss-minify-font-values: 6.1.0(postcss@8.5.25) - postcss-minify-gradients: 6.0.3(postcss@8.5.25) - postcss-minify-params: 6.1.0(postcss@8.5.25) - postcss-minify-selectors: 6.0.4(postcss@8.5.25) - postcss-normalize-charset: 6.0.2(postcss@8.5.25) - postcss-normalize-display-values: 6.0.2(postcss@8.5.25) - postcss-normalize-positions: 6.0.2(postcss@8.5.25) - postcss-normalize-repeat-style: 6.0.2(postcss@8.5.25) - postcss-normalize-string: 6.0.2(postcss@8.5.25) - postcss-normalize-timing-functions: 6.0.2(postcss@8.5.25) - postcss-normalize-unicode: 6.1.0(postcss@8.5.25) - postcss-normalize-url: 6.0.2(postcss@8.5.25) - postcss-normalize-whitespace: 6.0.2(postcss@8.5.25) - postcss-ordered-values: 6.0.2(postcss@8.5.25) - postcss-reduce-initial: 6.1.0(postcss@8.5.25) - postcss-reduce-transforms: 6.0.2(postcss@8.5.25) - postcss-svgo: 6.0.3(postcss@8.5.25) - postcss-unique-selectors: 6.0.4(postcss@8.5.25) + css-declaration-sorter: 7.4.0(postcss@8.5.26) + cssnano-utils: 4.0.2(postcss@8.5.26) + postcss: 8.5.26 + postcss-calc: 9.0.1(postcss@8.5.26) + postcss-colormin: 6.1.0(postcss@8.5.26) + postcss-convert-values: 6.1.0(postcss@8.5.26) + postcss-discard-comments: 6.0.2(postcss@8.5.26) + postcss-discard-duplicates: 6.0.3(postcss@8.5.26) + postcss-discard-empty: 6.0.3(postcss@8.5.26) + postcss-discard-overridden: 6.0.2(postcss@8.5.26) + postcss-merge-longhand: 6.0.5(postcss@8.5.26) + postcss-merge-rules: 6.1.1(postcss@8.5.26) + postcss-minify-font-values: 6.1.0(postcss@8.5.26) + postcss-minify-gradients: 6.0.3(postcss@8.5.26) + postcss-minify-params: 6.1.0(postcss@8.5.26) + postcss-minify-selectors: 6.0.4(postcss@8.5.26) + postcss-normalize-charset: 6.0.2(postcss@8.5.26) + postcss-normalize-display-values: 6.0.2(postcss@8.5.26) + postcss-normalize-positions: 6.0.2(postcss@8.5.26) + postcss-normalize-repeat-style: 6.0.2(postcss@8.5.26) + postcss-normalize-string: 6.0.2(postcss@8.5.26) + postcss-normalize-timing-functions: 6.0.2(postcss@8.5.26) + postcss-normalize-unicode: 6.1.0(postcss@8.5.26) + postcss-normalize-url: 6.0.2(postcss@8.5.26) + postcss-normalize-whitespace: 6.0.2(postcss@8.5.26) + postcss-ordered-values: 6.0.2(postcss@8.5.26) + postcss-reduce-initial: 6.1.0(postcss@8.5.26) + postcss-reduce-transforms: 6.0.2(postcss@8.5.26) + postcss-svgo: 6.0.3(postcss@8.5.26) + postcss-unique-selectors: 6.0.4(postcss@8.5.26) - cssnano-utils@4.0.2(postcss@8.5.25): + cssnano-utils@4.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - cssnano@6.1.2(postcss@8.5.25): + cssnano@6.1.2(postcss@8.5.26): dependencies: - cssnano-preset-default: 6.1.2(postcss@8.5.25) + cssnano-preset-default: 6.1.2(postcss@8.5.26) lilconfig: 3.1.3 - postcss: 8.5.25 + postcss: 8.5.26 csso@5.0.5: dependencies: @@ -20202,7 +20160,7 @@ snapshots: dependencies: address: 2.0.3 - devalue@5.8.2: {} + devalue@5.9.0: {} devlop@1.1.0: dependencies: @@ -20259,9 +20217,9 @@ snapshots: transitivePeerDependencies: - supports-color - docusaurus-lunr-search@3.6.0(@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + docusaurus-lunr-search@3.6.0(@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.18)(react@19.2.8))(@swc/core@1.15.47(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) autocomplete.js: 0.37.1 clsx: 2.1.1 gauge: 3.0.2 @@ -20547,34 +20505,34 @@ snapshots: '@esbuild/win32-ia32': 0.27.4 '@esbuild/win32-x64': 0.27.4 - esbuild@0.28.1: + esbuild@0.28.2: optionalDependencies: - '@esbuild/aix-ppc64': 0.28.1 - '@esbuild/android-arm': 0.28.1 - '@esbuild/android-arm64': 0.28.1 - '@esbuild/android-x64': 0.28.1 - '@esbuild/darwin-arm64': 0.28.1 - '@esbuild/darwin-x64': 0.28.1 - '@esbuild/freebsd-arm64': 0.28.1 - '@esbuild/freebsd-x64': 0.28.1 - '@esbuild/linux-arm': 0.28.1 - '@esbuild/linux-arm64': 0.28.1 - '@esbuild/linux-ia32': 0.28.1 - '@esbuild/linux-loong64': 0.28.1 - '@esbuild/linux-mips64el': 0.28.1 - '@esbuild/linux-ppc64': 0.28.1 - '@esbuild/linux-riscv64': 0.28.1 - '@esbuild/linux-s390x': 0.28.1 - '@esbuild/linux-x64': 0.28.1 - '@esbuild/netbsd-arm64': 0.28.1 - '@esbuild/netbsd-x64': 0.28.1 - '@esbuild/openbsd-arm64': 0.28.1 - '@esbuild/openbsd-x64': 0.28.1 - '@esbuild/openharmony-arm64': 0.28.1 - '@esbuild/sunos-x64': 0.28.1 - '@esbuild/win32-arm64': 0.28.1 - '@esbuild/win32-ia32': 0.28.1 - '@esbuild/win32-x64': 0.28.1 + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 escalade@3.2.0: {} @@ -20630,7 +20588,7 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) - eslint-plugin-svelte@3.22.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + eslint-plugin-svelte@3.22.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@jridgewell/sourcemap-codec': 1.5.5 @@ -20638,13 +20596,13 @@ snapshots: esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 - postcss: 8.5.25 - postcss-load-config: 3.1.4(postcss@8.5.25) - postcss-safe-parser: 7.0.1(postcss@8.5.25) + postcss: 8.5.26 + postcss-load-config: 3.1.4(postcss@8.5.26) + postcss-safe-parser: 7.0.1(postcss@8.5.26) semver: 7.8.5 - svelte-eslint-parser: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + svelte-eslint-parser: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) optionalDependencies: - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) transitivePeerDependencies: - ts-node @@ -20660,7 +20618,7 @@ snapshots: entities: 4.5.0 eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) find-up-simple: 1.0.1 - globals: 17.7.0 + globals: 17.9.0 indent-string: 5.0.0 is-builtin-module: 5.0.0 is-identifier: 1.1.0 @@ -20759,11 +20717,11 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.13(@typescript-eslint/types@8.65.0): + esrap@2.2.13(@typescript-eslint/types@8.66.0): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 optionalDependencies: - '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/types': 8.66.0 esrecurse@4.3.0: dependencies: @@ -20862,7 +20820,7 @@ snapshots: exiftool-vendored@35.21.0: dependencies: '@photostructure/tz-lookup': 11.5.0 - '@types/luxon': 3.7.2 + '@types/luxon': 3.7.3 batch-cluster: 17.3.1 exiftool-vendored.pl: 13.59.0 he: 1.2.0 @@ -21022,11 +20980,11 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) file-source@0.6.1: dependencies: @@ -21113,7 +21071,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@babel/code-frame': 7.29.7 chalk: 4.1.2 @@ -21128,7 +21086,7 @@ snapshots: semver: 7.8.5 tapable: 2.3.3 typescript: 5.9.3 - webpack: 5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) form-data-encoder@2.1.4: {} @@ -21190,6 +21148,8 @@ snapshots: function-timeout@1.0.2: {} + fuse.js@7.5.0: {} + gauge@3.0.2: dependencies: aproba: 2.1.0 @@ -21310,7 +21270,7 @@ snapshots: globals@16.5.0: {} - globals@17.7.0: {} + globals@17.9.0: {} globalyzer@0.1.0: {} @@ -21567,10 +21527,10 @@ snapshots: hls-video-element@1.5.11: dependencies: custom-media-element: 1.4.6 - hls.js: 1.6.16 + hls.js: 1.6.17 media-tracks: 0.3.5 - hls.js@1.6.16: {} + hls.js@1.6.17: {} hogan.js@3.0.2: dependencies: @@ -21627,7 +21587,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.7(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + html-webpack-plugin@5.6.7(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -21635,7 +21595,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.3.3 optionalDependencies: - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) html5parser@3.0.0: {} @@ -21764,9 +21724,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.25): + icss-utils@5.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 identifier-regex@1.1.0: dependencies: @@ -21856,10 +21816,10 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.11.4 tslib: 2.8.1 - intl-messageformat@11.2.12: + intl-messageformat@11.2.13: dependencies: '@formatjs/fast-memoize': 3.1.7 - '@formatjs/icu-messageformat-parser': 3.5.15 + '@formatjs/icu-messageformat-parser': 3.5.16 invariant@2.2.4: dependencies: @@ -22074,7 +22034,7 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 - jose@6.2.4: {} + jose@6.2.8: {} js-tokens@10.0.0: {} @@ -22090,7 +22050,7 @@ snapshots: dependencies: argparse: 2.0.1 - js-yaml@5.2.2: + js-yaml@5.2.3: dependencies: argparse: 2.0.1 @@ -23176,11 +23136,11 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.10.2(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + mini-css-extract-plugin@2.10.2(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: schema-utils: 4.3.3 tapable: 2.3.3 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) minimalistic-assert@1.0.1: {} @@ -23202,22 +23162,22 @@ snapshots: minimist@1.2.8: {} - minimizer-webpack-plugin@5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + minimizer-webpack-plugin@5.6.1(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.49.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) optionalDependencies: - '@swc/core': 1.15.46(@swc/helpers@0.5.23) + '@swc/core': 1.15.47(@swc/helpers@0.5.23) clean-css: 5.3.3 - cssnano: 6.1.2(postcss@8.5.25) + cssnano: 6.1.2(postcss@8.5.26) csso: 5.0.5 - esbuild: 0.28.1 + esbuild: 0.28.2 html-minifier-terser: 7.2.0 lightningcss: 1.33.0 - postcss: 8.5.25 + postcss: 8.5.26 uglify-js: 3.19.3 minipass@3.3.6: @@ -23275,7 +23235,7 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 optional: true - msgpackr@2.0.4: + msgpackr@2.0.5: optionalDependencies: msgpackr-extract: 3.0.4 @@ -23308,9 +23268,9 @@ snapshots: nan@2.28.0: optional: true - nanoid@3.3.16: {} + nanoid@3.3.18: {} - nanoid@6.0.0: {} + nanoid@6.0.1: {} napi-build-utils@2.0.0: optional: true @@ -23439,7 +23399,7 @@ snapshots: node-releases@2.0.51: {} - nodemailer@9.0.3: {} + nodemailer@9.0.4: {} nopt@1.0.10: dependencies: @@ -23478,11 +23438,11 @@ snapshots: dependencies: boolbase: 1.0.0 - null-loader@4.0.1(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + null-loader@4.0.1(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) nwsapi@2.2.24: optional: true @@ -23518,16 +23478,16 @@ snapshots: obug@2.1.3: {} - oidc-provider@9.10.0(supports-color@8.1.1): + oidc-provider@9.11.2(supports-color@8.1.1): dependencies: '@koa/cors': 5.0.0 '@koa/router': 15.7.0(koa@3.2.1)(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1) eta: 4.6.0 - jose: 6.2.4 + jose: 6.2.8 jsesc: 3.1.0 koa: 3.2.1 - nanoid: 6.0.0 + nanoid: 6.0.1 quick-lru: 7.3.0 raw-body: 4.0.0 transitivePeerDependencies: @@ -23577,7 +23537,7 @@ snapshots: openid-client@6.8.4: dependencies: - jose: 6.2.4 + jose: 6.2.8 oauth4webapi: 3.8.6 optionator@0.9.4: @@ -23807,7 +23767,7 @@ snapshots: dependencies: pg: 8.22.0 - pg-protocol@1.15.0: {} + pg-protocol@1.16.0: {} pg-types@2.2.0: dependencies: @@ -23821,7 +23781,7 @@ snapshots: dependencies: pg-connection-string: 2.14.0 pg-pool: 3.14.0(pg@8.22.0) - pg-protocol: 1.15.0 + pg-protocol: 1.16.0 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: @@ -23856,11 +23816,11 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 - playwright-core@1.62.0: {} + playwright-core@1.62.1: {} - playwright@1.62.0: + playwright@1.62.1: dependencies: - playwright-core: 1.62.0 + playwright-core: 1.62.1 optionalDependencies: fsevents: 2.3.2 @@ -23894,448 +23854,448 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - postcss-attribute-case-insensitive@7.0.1(postcss@8.5.25): + postcss-attribute-case-insensitive@7.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-calc@9.0.1(postcss@8.5.25): + postcss-calc@9.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 6.1.4 postcss-value-parser: 4.2.0 - postcss-clamp@4.1.0(postcss@8.5.25): + postcss-clamp@4.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-color-functional-notation@7.0.12(postcss@8.5.25): + postcss-color-functional-notation@7.0.12(postcss@8.5.26): dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - postcss-color-hex-alpha@10.0.0(postcss@8.5.25): + postcss-color-hex-alpha@10.0.0(postcss@8.5.26): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-color-rebeccapurple@10.0.0(postcss@8.5.25): + postcss-color-rebeccapurple@10.0.0(postcss@8.5.26): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-colormin@6.1.0(postcss@8.5.25): + postcss-colormin@6.1.0(postcss@8.5.26): dependencies: browserslist: 4.28.6 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-convert-values@6.1.0(postcss@8.5.25): + postcss-convert-values@6.1.0(postcss@8.5.26): dependencies: browserslist: 4.28.6 - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-custom-media@11.0.6(postcss@8.5.25): + postcss-custom-media@11.0.6(postcss@8.5.26): dependencies: '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - postcss: 8.5.25 + postcss: 8.5.26 - postcss-custom-properties@14.0.6(postcss@8.5.25): + postcss-custom-properties@14.0.6(postcss@8.5.26): dependencies: '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-custom-selectors@8.0.5(postcss@8.5.25): + postcss-custom-selectors@8.0.5(postcss@8.5.26): dependencies: '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-dir-pseudo-class@9.0.1(postcss@8.5.25): + postcss-dir-pseudo-class@9.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-discard-comments@6.0.2(postcss@8.5.25): + postcss-discard-comments@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-discard-duplicates@6.0.3(postcss@8.5.25): + postcss-discard-duplicates@6.0.3(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-discard-empty@6.0.3(postcss@8.5.25): + postcss-discard-empty@6.0.3(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-discard-overridden@6.0.2(postcss@8.5.25): + postcss-discard-overridden@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-discard-unused@6.0.5(postcss@8.5.25): + postcss-discard-unused@6.0.5(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 6.1.4 - postcss-double-position-gradients@6.0.4(postcss@8.5.25): + postcss-double-position-gradients@6.0.4(postcss@8.5.26): dependencies: - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-focus-visible@10.0.1(postcss@8.5.25): + postcss-focus-visible@10.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-focus-within@9.0.1(postcss@8.5.25): + postcss-focus-within@9.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-font-variant@5.0.0(postcss@8.5.25): + postcss-font-variant@5.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-gap-properties@6.0.0(postcss@8.5.25): + postcss-gap-properties@6.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-image-set-function@7.0.0(postcss@8.5.25): + postcss-image-set-function@7.0.0(postcss@8.5.26): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-import@15.1.0(postcss@8.5.25): + postcss-import@15.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.12 - postcss-js@4.1.0(postcss@8.5.25): + postcss-js@4.1.0(postcss@8.5.26): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.25 + postcss: 8.5.26 - postcss-lab-function@7.0.12(postcss@8.5.25): + postcss-lab-function@7.0.12(postcss@8.5.26): dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/utilities': 2.0.0(postcss@8.5.25) - postcss: 8.5.25 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/utilities': 2.0.0(postcss@8.5.26) + postcss: 8.5.26 - postcss-load-config@3.1.4(postcss@8.5.25): + postcss-load-config@3.1.4(postcss@8.5.26): dependencies: lilconfig: 2.1.0 yaml: 1.10.3 optionalDependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.25)(tsx@4.23.1)(yaml@2.9.0): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.26)(tsx@4.23.9)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 - postcss: 8.5.25 - tsx: 4.23.1 + postcss: 8.5.26 + tsx: 4.23.9 yaml: 2.9.0 - postcss-loader@7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.25)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + postcss-loader@7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.26)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: cosmiconfig: 8.3.6(@typescript/typescript6@6.0.2) jiti: 1.21.7 - postcss: 8.5.25 + postcss: 8.5.26 semver: 7.8.5 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - typescript - postcss-logical@8.1.0(postcss@8.5.25): + postcss-logical@8.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-merge-idents@6.0.3(postcss@8.5.25): + postcss-merge-idents@6.0.3(postcss@8.5.26): dependencies: - cssnano-utils: 4.0.2(postcss@8.5.25) - postcss: 8.5.25 + cssnano-utils: 4.0.2(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-merge-longhand@6.0.5(postcss@8.5.25): + postcss-merge-longhand@6.0.5(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - stylehacks: 6.1.1(postcss@8.5.25) + stylehacks: 6.1.1(postcss@8.5.26) - postcss-merge-rules@6.1.1(postcss@8.5.25): + postcss-merge-rules@6.1.1(postcss@8.5.26): dependencies: browserslist: 4.28.6 caniuse-api: 3.0.0 - cssnano-utils: 4.0.2(postcss@8.5.25) - postcss: 8.5.25 + cssnano-utils: 4.0.2(postcss@8.5.26) + postcss: 8.5.26 postcss-selector-parser: 6.1.4 - postcss-minify-font-values@6.1.0(postcss@8.5.25): + postcss-minify-font-values@6.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-minify-gradients@6.0.3(postcss@8.5.25): + postcss-minify-gradients@6.0.3(postcss@8.5.26): dependencies: colord: 2.9.3 - cssnano-utils: 4.0.2(postcss@8.5.25) - postcss: 8.5.25 + cssnano-utils: 4.0.2(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-minify-params@6.1.0(postcss@8.5.25): + postcss-minify-params@6.1.0(postcss@8.5.26): dependencies: browserslist: 4.28.6 - cssnano-utils: 4.0.2(postcss@8.5.25) - postcss: 8.5.25 + cssnano-utils: 4.0.2(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-minify-selectors@6.0.4(postcss@8.5.25): + postcss-minify-selectors@6.0.4(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 6.1.4 - postcss-modules-extract-imports@3.1.0(postcss@8.5.25): + postcss-modules-extract-imports@3.1.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-modules-local-by-default@4.2.0(postcss@8.5.25): + postcss-modules-local-by-default@4.2.0(postcss@8.5.26): dependencies: - icss-utils: 5.1.0(postcss@8.5.25) - postcss: 8.5.25 + icss-utils: 5.1.0(postcss@8.5.26) + postcss: 8.5.26 postcss-selector-parser: 7.1.4 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.25): + postcss-modules-scope@3.2.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-modules-values@4.0.0(postcss@8.5.25): + postcss-modules-values@4.0.0(postcss@8.5.26): dependencies: - icss-utils: 5.1.0(postcss@8.5.25) - postcss: 8.5.25 + icss-utils: 5.1.0(postcss@8.5.26) + postcss: 8.5.26 - postcss-nested@6.2.0(postcss@8.5.25): + postcss-nested@6.2.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 6.1.4 - postcss-nesting@13.0.2(postcss@8.5.25): + postcss-nesting@13.0.2(postcss@8.5.26): dependencies: '@csstools/selector-resolve-nested': 3.1.0(postcss-selector-parser@7.1.4) '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-normalize-charset@6.0.2(postcss@8.5.25): + postcss-normalize-charset@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-normalize-display-values@6.0.2(postcss@8.5.25): + postcss-normalize-display-values@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-positions@6.0.2(postcss@8.5.25): + postcss-normalize-positions@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@6.0.2(postcss@8.5.25): + postcss-normalize-repeat-style@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-string@6.0.2(postcss@8.5.25): + postcss-normalize-string@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@6.0.2(postcss@8.5.25): + postcss-normalize-timing-functions@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@6.1.0(postcss@8.5.25): + postcss-normalize-unicode@6.1.0(postcss@8.5.26): dependencies: browserslist: 4.28.6 - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-url@6.0.2(postcss@8.5.25): + postcss-normalize-url@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@6.0.2(postcss@8.5.25): + postcss-normalize-whitespace@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-opacity-percentage@3.0.0(postcss@8.5.25): + postcss-opacity-percentage@3.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-ordered-values@6.0.2(postcss@8.5.25): + postcss-ordered-values@6.0.2(postcss@8.5.26): dependencies: - cssnano-utils: 4.0.2(postcss@8.5.25) - postcss: 8.5.25 + cssnano-utils: 4.0.2(postcss@8.5.26) + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-overflow-shorthand@6.0.0(postcss@8.5.25): + postcss-overflow-shorthand@6.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-page-break@3.0.4(postcss@8.5.25): + postcss-page-break@3.0.4(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-place@10.0.0(postcss@8.5.25): + postcss-place@10.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-preset-env@10.6.1(postcss@8.5.25): + postcss-preset-env@10.6.1(postcss@8.5.26): dependencies: - '@csstools/postcss-alpha-function': 1.0.1(postcss@8.5.25) - '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.25) - '@csstools/postcss-color-function': 4.0.12(postcss@8.5.25) - '@csstools/postcss-color-function-display-p3-linear': 1.0.1(postcss@8.5.25) - '@csstools/postcss-color-mix-function': 3.0.12(postcss@8.5.25) - '@csstools/postcss-color-mix-variadic-function-arguments': 1.0.2(postcss@8.5.25) - '@csstools/postcss-content-alt-text': 2.0.8(postcss@8.5.25) - '@csstools/postcss-contrast-color-function': 2.0.12(postcss@8.5.25) - '@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.25) - '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.25) - '@csstools/postcss-gamut-mapping': 2.0.11(postcss@8.5.25) - '@csstools/postcss-gradients-interpolation-method': 5.0.12(postcss@8.5.25) - '@csstools/postcss-hwb-function': 4.0.12(postcss@8.5.25) - '@csstools/postcss-ic-unit': 4.0.4(postcss@8.5.25) - '@csstools/postcss-initial': 2.0.1(postcss@8.5.25) - '@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.25) - '@csstools/postcss-light-dark-function': 2.0.11(postcss@8.5.25) - '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.25) - '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.25) - '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.25) - '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.25) - '@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.25) - '@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.25) - '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.25) - '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.25) - '@csstools/postcss-normalize-display-values': 4.0.1(postcss@8.5.25) - '@csstools/postcss-oklab-function': 4.0.12(postcss@8.5.25) - '@csstools/postcss-position-area-property': 1.0.0(postcss@8.5.25) - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) - '@csstools/postcss-property-rule-prelude-list': 1.0.0(postcss@8.5.25) - '@csstools/postcss-random-function': 2.0.1(postcss@8.5.25) - '@csstools/postcss-relative-color-syntax': 3.0.12(postcss@8.5.25) - '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.25) - '@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.25) - '@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.25) - '@csstools/postcss-syntax-descriptor-syntax-production': 1.0.1(postcss@8.5.25) - '@csstools/postcss-system-ui-font-family': 1.0.0(postcss@8.5.25) - '@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.25) - '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.25) - '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.25) - autoprefixer: 10.5.4(postcss@8.5.25) + '@csstools/postcss-alpha-function': 1.0.1(postcss@8.5.26) + '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.26) + '@csstools/postcss-color-function': 4.0.12(postcss@8.5.26) + '@csstools/postcss-color-function-display-p3-linear': 1.0.1(postcss@8.5.26) + '@csstools/postcss-color-mix-function': 3.0.12(postcss@8.5.26) + '@csstools/postcss-color-mix-variadic-function-arguments': 1.0.2(postcss@8.5.26) + '@csstools/postcss-content-alt-text': 2.0.8(postcss@8.5.26) + '@csstools/postcss-contrast-color-function': 2.0.12(postcss@8.5.26) + '@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.26) + '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.26) + '@csstools/postcss-gamut-mapping': 2.0.11(postcss@8.5.26) + '@csstools/postcss-gradients-interpolation-method': 5.0.12(postcss@8.5.26) + '@csstools/postcss-hwb-function': 4.0.12(postcss@8.5.26) + '@csstools/postcss-ic-unit': 4.0.4(postcss@8.5.26) + '@csstools/postcss-initial': 2.0.1(postcss@8.5.26) + '@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.26) + '@csstools/postcss-light-dark-function': 2.0.11(postcss@8.5.26) + '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.26) + '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.26) + '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.26) + '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.26) + '@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.26) + '@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.26) + '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.26) + '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.26) + '@csstools/postcss-normalize-display-values': 4.0.1(postcss@8.5.26) + '@csstools/postcss-oklab-function': 4.0.12(postcss@8.5.26) + '@csstools/postcss-position-area-property': 1.0.0(postcss@8.5.26) + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.26) + '@csstools/postcss-property-rule-prelude-list': 1.0.0(postcss@8.5.26) + '@csstools/postcss-random-function': 2.0.1(postcss@8.5.26) + '@csstools/postcss-relative-color-syntax': 3.0.12(postcss@8.5.26) + '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.26) + '@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.26) + '@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.26) + '@csstools/postcss-syntax-descriptor-syntax-production': 1.0.1(postcss@8.5.26) + '@csstools/postcss-system-ui-font-family': 1.0.0(postcss@8.5.26) + '@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.26) + '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.26) + '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.26) + autoprefixer: 10.5.4(postcss@8.5.26) browserslist: 4.28.6 - css-blank-pseudo: 7.0.1(postcss@8.5.25) - css-has-pseudo: 7.0.3(postcss@8.5.25) - css-prefers-color-scheme: 10.0.0(postcss@8.5.25) + css-blank-pseudo: 7.0.1(postcss@8.5.26) + css-has-pseudo: 7.0.3(postcss@8.5.26) + css-prefers-color-scheme: 10.0.0(postcss@8.5.26) cssdb: 8.9.0 - postcss: 8.5.25 - postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.25) - postcss-clamp: 4.1.0(postcss@8.5.25) - postcss-color-functional-notation: 7.0.12(postcss@8.5.25) - postcss-color-hex-alpha: 10.0.0(postcss@8.5.25) - postcss-color-rebeccapurple: 10.0.0(postcss@8.5.25) - postcss-custom-media: 11.0.6(postcss@8.5.25) - postcss-custom-properties: 14.0.6(postcss@8.5.25) - postcss-custom-selectors: 8.0.5(postcss@8.5.25) - postcss-dir-pseudo-class: 9.0.1(postcss@8.5.25) - postcss-double-position-gradients: 6.0.4(postcss@8.5.25) - postcss-focus-visible: 10.0.1(postcss@8.5.25) - postcss-focus-within: 9.0.1(postcss@8.5.25) - postcss-font-variant: 5.0.0(postcss@8.5.25) - postcss-gap-properties: 6.0.0(postcss@8.5.25) - postcss-image-set-function: 7.0.0(postcss@8.5.25) - postcss-lab-function: 7.0.12(postcss@8.5.25) - postcss-logical: 8.1.0(postcss@8.5.25) - postcss-nesting: 13.0.2(postcss@8.5.25) - postcss-opacity-percentage: 3.0.0(postcss@8.5.25) - postcss-overflow-shorthand: 6.0.0(postcss@8.5.25) - postcss-page-break: 3.0.4(postcss@8.5.25) - postcss-place: 10.0.0(postcss@8.5.25) - postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.25) - postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.25) - postcss-selector-not: 8.0.1(postcss@8.5.25) + postcss: 8.5.26 + postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.26) + postcss-clamp: 4.1.0(postcss@8.5.26) + postcss-color-functional-notation: 7.0.12(postcss@8.5.26) + postcss-color-hex-alpha: 10.0.0(postcss@8.5.26) + postcss-color-rebeccapurple: 10.0.0(postcss@8.5.26) + postcss-custom-media: 11.0.6(postcss@8.5.26) + postcss-custom-properties: 14.0.6(postcss@8.5.26) + postcss-custom-selectors: 8.0.5(postcss@8.5.26) + postcss-dir-pseudo-class: 9.0.1(postcss@8.5.26) + postcss-double-position-gradients: 6.0.4(postcss@8.5.26) + postcss-focus-visible: 10.0.1(postcss@8.5.26) + postcss-focus-within: 9.0.1(postcss@8.5.26) + postcss-font-variant: 5.0.0(postcss@8.5.26) + postcss-gap-properties: 6.0.0(postcss@8.5.26) + postcss-image-set-function: 7.0.0(postcss@8.5.26) + postcss-lab-function: 7.0.12(postcss@8.5.26) + postcss-logical: 8.1.0(postcss@8.5.26) + postcss-nesting: 13.0.2(postcss@8.5.26) + postcss-opacity-percentage: 3.0.0(postcss@8.5.26) + postcss-overflow-shorthand: 6.0.0(postcss@8.5.26) + postcss-page-break: 3.0.4(postcss@8.5.26) + postcss-place: 10.0.0(postcss@8.5.26) + postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.26) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.26) + postcss-selector-not: 8.0.1(postcss@8.5.26) - postcss-pseudo-class-any-link@10.0.1(postcss@8.5.25): + postcss-pseudo-class-any-link@10.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 - postcss-reduce-idents@6.0.3(postcss@8.5.25): + postcss-reduce-idents@6.0.3(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-reduce-initial@6.1.0(postcss@8.5.25): + postcss-reduce-initial@6.1.0(postcss@8.5.26): dependencies: browserslist: 4.28.6 caniuse-api: 3.0.0 - postcss: 8.5.25 + postcss: 8.5.26 - postcss-reduce-transforms@6.0.2(postcss@8.5.25): + postcss-reduce-transforms@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 - postcss-replace-overflow-wrap@4.0.0(postcss@8.5.25): + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-safe-parser@7.0.1(postcss@8.5.25): + postcss-safe-parser@7.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-scss@4.0.9(postcss@8.5.25): + postcss-scss@4.0.9(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss-selector-not@8.0.1(postcss@8.5.25): + postcss-selector-not@8.0.1(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 7.1.4 postcss-selector-parser@6.1.4: @@ -24348,31 +24308,31 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-sort-media-queries@5.2.0(postcss@8.5.25): + postcss-sort-media-queries@5.2.0(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 sort-css-media-queries: 2.2.0 - postcss-svgo@6.0.3(postcss@8.5.25): + postcss-svgo@6.0.3(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-value-parser: 4.2.0 svgo: 3.3.4 - postcss-unique-selectors@6.0.4(postcss@8.5.25): + postcss-unique-selectors@6.0.4(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 6.1.4 postcss-value-parser@4.2.0: {} - postcss-zindex@6.0.2(postcss@8.5.25): + postcss-zindex@6.0.2(postcss@8.5.26): dependencies: - postcss: 8.5.25 + postcss: 8.5.26 - postcss@8.5.25: + postcss@8.5.26: dependencies: - nanoid: 3.3.16 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -24423,10 +24383,10 @@ snapshots: dependencies: prettier: 3.9.6 - prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: prettier: 3.9.6 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) prettier@3.9.6: {} @@ -24588,11 +24548,11 @@ snapshots: bytes: 3.1.2 http-errors: 2.0.1 - raw-loader@4.0.2(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + raw-loader@4.0.2(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) rc@1.2.8: dependencies: @@ -24640,11 +24600,11 @@ snapshots: dependencies: react: 19.2.8 - react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.8))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.8))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@babel/runtime': 7.29.7 react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.8)' - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) react-router-config@5.1.1(react-router@5.3.4(react@19.2.8))(react@19.2.8): dependencies: @@ -24953,35 +24913,34 @@ snapshots: robust-predicates@3.0.3: {} - rolldown@1.1.5: + rolldown@1.2.3: dependencies: - '@oxc-project/types': 0.139.0 + '@oxc-project/types': 0.143.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.5 - '@rolldown/binding-darwin-arm64': 1.1.5 - '@rolldown/binding-darwin-x64': 1.1.5 - '@rolldown/binding-freebsd-x64': 1.1.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 - '@rolldown/binding-linux-arm64-gnu': 1.1.5 - '@rolldown/binding-linux-arm64-musl': 1.1.5 - '@rolldown/binding-linux-ppc64-gnu': 1.1.5 - '@rolldown/binding-linux-s390x-gnu': 1.1.5 - '@rolldown/binding-linux-x64-gnu': 1.1.5 - '@rolldown/binding-linux-x64-musl': 1.1.5 - '@rolldown/binding-openharmony-arm64': 1.1.5 - '@rolldown/binding-wasm32-wasi': 1.1.5 - '@rolldown/binding-win32-arm64-msvc': 1.1.5 - '@rolldown/binding-win32-x64-msvc': 1.1.5 + '@rolldown/binding-android-arm64': 1.2.3 + '@rolldown/binding-darwin-arm64': 1.2.3 + '@rolldown/binding-darwin-x64': 1.2.3 + '@rolldown/binding-freebsd-x64': 1.2.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.3 + '@rolldown/binding-linux-arm64-gnu': 1.2.3 + '@rolldown/binding-linux-arm64-musl': 1.2.3 + '@rolldown/binding-linux-ppc64-gnu': 1.2.3 + '@rolldown/binding-linux-s390x-gnu': 1.2.3 + '@rolldown/binding-linux-x64-gnu': 1.2.3 + '@rolldown/binding-linux-x64-musl': 1.2.3 + '@rolldown/binding-openharmony-arm64': 1.2.3 + '@rolldown/binding-win32-arm64-msvc': 1.2.3 + '@rolldown/binding-win32-x64-msvc': 1.2.3 - rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.0): + rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.62.0): dependencies: open: 11.0.0 picomatch: 4.0.5 source-map: 0.7.6 yargs: 18.0.0 optionalDependencies: - rolldown: 1.1.5 + rolldown: 1.2.3 rollup: 4.62.0 rollup@4.62.0: @@ -25039,7 +24998,7 @@ snapshots: dependencies: escalade: 3.2.0 picocolors: 1.1.1 - postcss: 8.5.25 + postcss: 8.5.26 strip-json-comments: 3.1.1 run-applescript@7.1.0: {} @@ -25050,14 +25009,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.35.1(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + runed@0.35.1(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) optionalDependencies: - '@sveltejs/kit': 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@sveltejs/kit': 2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) rw@1.3.3: {} @@ -25334,7 +25293,7 @@ snapshots: simple-concat: 1.0.1 optional: true - simple-icons@16.27.1: {} + simple-icons@16.28.0: {} sirv@2.0.4: dependencies: @@ -25595,10 +25554,10 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - stylehacks@6.1.1(postcss@8.5.25): + stylehacks@6.1.1(postcss@8.5.26): dependencies: browserslist: 4.28.6 - postcss: 8.5.25 + postcss: 8.5.26 postcss-selector-parser: 6.1.4 stylis@4.4.0: {} @@ -25651,34 +25610,34 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-awesome@3.3.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-awesome@3.3.5(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - svelte-check@4.7.3(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-check@4.7.4(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: '@jridgewell/trace-mapping': 0.3.31 - '@sveltejs/load-config': 0.2.0 + '@sveltejs/load-config': 0.2.2 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.5) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-eslint-parser@1.8.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 - postcss: 8.5.25 - postcss-scss: 4.0.9(postcss@8.5.25) + postcss: 8.5.26 + postcss-scss: 4.0.9(postcss@8.5.26) postcss-selector-parser: 7.1.4 semver: 7.8.5 optionalDependencies: - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) svelte-floating-ui@1.5.8: dependencies: @@ -25691,7 +25650,7 @@ snapshots: dependencies: highlight.js: 11.11.1 - svelte-i18n@4.0.1(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-i18n@4.0.1(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: cli-color: 2.0.4 deepmerge: 4.3.1 @@ -25699,10 +25658,10 @@ snapshots: estree-walker: 2.0.2 intl-messageformat: 10.7.18 sade: 1.8.1 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) tiny-glob: 0.2.9 - svelte-jsoneditor@3.13.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-jsoneditor@3.13.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: '@codemirror/autocomplete': 6.20.3 '@codemirror/commands': 6.10.4 @@ -25729,55 +25688,55 @@ snapshots: memoize-one: 6.0.0 natural-compare-lite: 1.4.0 sass: 1.102.0 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) - svelte-awesome: 3.3.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + svelte-awesome: 3.3.5(svelte@5.56.8(@typescript-eslint/types@8.66.0)) svelte-select: 5.8.3 vanilla-picker: 2.12.3 - svelte-maplibre@1.3.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-maplibre@1.3.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: d3-geo: 3.1.1 dequal: 2.0.3 just-compare: 2.3.0 maplibre-gl: 5.24.0 pmtiles: 3.2.1 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - svelte-parse-markup@0.1.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-parse-markup@0.1.5(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - svelte-persisted-store@0.12.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-persisted-store@0.12.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) svelte-select@5.8.3: dependencies: svelte-floating-ui: 1.5.8 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + runed: 0.35.1(@sveltejs/kit@2.70.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) style-to-object: 1.0.14 - svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) transitivePeerDependencies: - '@sveltejs/kit' - svelte@5.56.8(@typescript-eslint/types@8.65.0): + svelte@5.56.8(@typescript-eslint/types@8.66.0): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.11(acorn@8.18.0) + '@sveltejs/acorn-typescript': 1.0.12(acorn@8.18.0) '@types/estree': 1.0.9 '@types/trusted-types': 2.0.7 acorn: 8.18.0 aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.8.2 + devalue: 5.9.0 esm-env: 1.2.2 - esrap: 2.2.13(@typescript-eslint/types@8.65.0) + esrap: 2.2.13(@typescript-eslint/types@8.66.0) is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 @@ -25820,27 +25779,26 @@ snapshots: tailwind-merge@3.6.0: {} - tailwind-variants@3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3): - dependencies: - tailwindcss: 4.3.3 + tailwind-variants@3.3.1(tailwind-merge@3.6.0)(tailwindcss@4.3.3): optionalDependencies: tailwind-merge: 3.6.0 + tailwindcss: 4.3.3 - tailwindcss-email-variants@3.0.5(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0)): + tailwindcss-email-variants@3.0.5(tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0)): dependencies: - tailwindcss: 3.4.19(tsx@4.23.1)(yaml@2.9.0) + tailwindcss: 3.4.19(tsx@4.23.9)(yaml@2.9.0) - tailwindcss-mso@2.0.3(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0)): + tailwindcss-mso@2.0.3(tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0)): dependencies: - tailwindcss: 3.4.19(tsx@4.23.1)(yaml@2.9.0) + tailwindcss: 3.4.19(tsx@4.23.9)(yaml@2.9.0) - tailwindcss-preset-email@1.4.1(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0)): + tailwindcss-preset-email@1.4.1(tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0)): dependencies: - tailwindcss: 3.4.19(tsx@4.23.1)(yaml@2.9.0) - tailwindcss-email-variants: 3.0.5(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0)) - tailwindcss-mso: 2.0.3(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0)) + tailwindcss: 3.4.19(tsx@4.23.9)(yaml@2.9.0) + tailwindcss-email-variants: 3.0.5(tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0)) + tailwindcss-mso: 2.0.3(tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0)) - tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0): + tailwindcss@3.4.19(tsx@4.23.9)(yaml@2.9.0): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -25856,11 +25814,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.25 - postcss-import: 15.1.0(postcss@8.5.25) - postcss-js: 4.1.0(postcss@8.5.25) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.25)(tsx@4.23.1)(yaml@2.9.0) - postcss-nested: 6.2.0(postcss@8.5.25) + postcss: 8.5.26 + postcss-import: 15.1.0(postcss@8.5.26) + postcss-js: 4.1.0(postcss@8.5.26) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.26)(tsx@4.23.9)(yaml@2.9.0) + postcss-nested: 6.2.0(postcss@8.5.26) postcss-selector-parser: 6.1.4 resolve: 1.22.12 sucrase: 3.35.1 @@ -25884,7 +25842,7 @@ snapshots: pump: 3.0.4 tar-stream: 3.2.0 optionalDependencies: - bare-fs: 4.7.4 + bare-fs: 4.8.0 bare-path: 3.1.1 transitivePeerDependencies: - bare-abort-controller @@ -25902,7 +25860,7 @@ snapshots: tar-stream@3.2.0: dependencies: b4a: 1.8.1 - bare-fs: 4.7.4 + bare-fs: 4.8.0 fast-fifo: 1.3.2 streamx: 2.28.0 transitivePeerDependencies: @@ -25934,40 +25892,40 @@ snapshots: - bare-abort-controller - react-native-b4a - terser-webpack-plugin@5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + terser-webpack-plugin@5.6.1(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)(webpack@5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.49.0 - webpack: 5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) optionalDependencies: - '@swc/core': 1.15.46(@swc/helpers@0.5.23) + '@swc/core': 1.15.47(@swc/helpers@0.5.23) clean-css: 5.3.3 - cssnano: 6.1.2(postcss@8.5.25) + cssnano: 6.1.2(postcss@8.5.26) csso: 5.0.5 - esbuild: 0.28.1 + esbuild: 0.28.2 html-minifier-terser: 7.2.0 lightningcss: 1.33.0 - postcss: 8.5.25 + postcss: 8.5.26 uglify-js: 3.19.3 - terser-webpack-plugin@5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + terser-webpack-plugin@5.6.1(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.49.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) optionalDependencies: - '@swc/core': 1.15.46(@swc/helpers@0.5.23) + '@swc/core': 1.15.47(@swc/helpers@0.5.23) clean-css: 5.3.3 - cssnano: 6.1.2(postcss@8.5.25) + cssnano: 6.1.2(postcss@8.5.26) csso: 5.0.5 - esbuild: 0.28.1 + esbuild: 0.28.2 html-minifier-terser: 7.2.0 lightningcss: 1.33.0 - postcss: 8.5.25 + postcss: 8.5.26 uglify-js: 3.19.3 terser@5.49.0: @@ -25977,7 +25935,7 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - testcontainers@12.0.4(supports-color@8.1.1): + testcontainers@12.1.0(supports-color@8.1.1): dependencies: '@balena/dockerignore': 1.0.2 '@types/dockerode': 4.0.1 @@ -25993,7 +25951,7 @@ snapshots: ssh-remote-port-forward: 1.0.4 tar-fs: 3.1.3 tmp: 0.2.7 - undici: 8.7.0 + undici: 8.10.0 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -26165,9 +26123,9 @@ snapshots: tsscmp@1.0.6: {} - tsx@4.23.1: + tsx@4.23.9: dependencies: - esbuild: 0.28.1 + esbuild: 0.28.2 optionalDependencies: fsevents: 2.3.3 @@ -26217,12 +26175,12 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): + typescript-eslint@8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/parser': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/parser': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/typescript-estree': 8.66.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/utils': 8.66.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: @@ -26280,7 +26238,7 @@ snapshots: undici@6.26.0: {} - undici@8.7.0: {} + undici@8.10.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -26371,10 +26329,10 @@ snapshots: unpipe@1.0.0: {} - unplugin-swc@1.5.9(@swc/core@1.15.46(@swc/helpers@0.5.23))(rollup@4.62.0): + unplugin-swc@1.5.10(@swc/core@1.15.47(@swc/helpers@0.5.23))(rollup@4.62.0): dependencies: '@rollup/pluginutils': 5.4.0(rollup@4.62.0) - '@swc/core': 1.15.46(@swc/helpers@0.5.23) + '@swc/core': 1.15.47(@swc/helpers@0.5.23) load-tsconfig: 0.2.5 unplugin: 2.3.11 transitivePeerDependencies: @@ -26416,14 +26374,14 @@ snapshots: dependencies: punycode: 2.3.1 - url-loader@4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) optionalDependencies: - file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) url@0.11.4: dependencies: @@ -26505,13 +26463,13 @@ snapshots: transitivePeerDependencies: - rollup - vite-node@3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): + vite-node@3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -26526,22 +26484,22 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@6.1.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): + vite-tsconfig-paths@6.1.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1)(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)): dependencies: debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(@typescript/typescript6@6.0.2) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) transitivePeerDependencies: - supports-color - typescript - vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): + vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0): dependencies: - esbuild: 0.28.1 + esbuild: 0.28.2 fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 - postcss: 8.5.25 + postcss: 8.5.26 rollup: 4.62.0 tinyglobby: 0.2.17 optionalDependencies: @@ -26551,39 +26509,39 @@ snapshots: lightningcss: 1.33.0 sass: 1.102.0 terser: 5.49.0 - tsx: 4.23.1 + tsx: 4.23.9 yaml: 2.9.0 - vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): + vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.25 - rolldown: 1.1.5 + postcss: 8.5.26 + rolldown: 1.2.3 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.3 - esbuild: 0.28.1 + esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 sass: 1.102.0 terser: 5.49.0 - tsx: 4.23.1 + tsx: 4.23.9 yaml: 2.9.0 - vitefu@1.1.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): + vitefu@1.1.3(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)): optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) vitest-fetch-mock@0.4.5(vitest@4.1.10): dependencies: - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) - vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): + vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 @@ -26601,8 +26559,8 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 @@ -26623,10 +26581,10 @@ snapshots: - tsx - yaml - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -26643,7 +26601,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -26654,10 +26612,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3))(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -26674,7 +26632,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.9)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -26733,7 +26691,7 @@ snapshots: - bufferutil - utf-8-validate - webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: colorette: 2.0.20 memfs: 4.64.0(tslib@2.8.1) @@ -26742,11 +26700,11 @@ snapshots: range-parser: 1.3.0 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - tslib - webpack-dev-server@5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + webpack-dev-server@5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -26774,10 +26732,10 @@ snapshots: serve-index: 1.9.2(supports-color@8.1.1) sockjs: 0.3.24 spdy: 4.0.2(supports-color@8.1.1) - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) ws: 8.21.1 optionalDependencies: - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) transitivePeerDependencies: - bufferutil - debug @@ -26803,7 +26761,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3): + webpack@5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.9 @@ -26826,7 +26784,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + terser-webpack-plugin: 5.6.1(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)(webpack@5.106.2(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) watchpack: 2.5.2 webpack-sources: 3.5.1 transitivePeerDependencies: @@ -26843,7 +26801,7 @@ snapshots: - postcss - uglify-js - webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3): + webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3): dependencies: '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 @@ -26861,7 +26819,7 @@ snapshots: graceful-fs: 4.2.11 loader-runner: 4.3.2 mime-db: 1.54.0 - minimizer-webpack-plugin: 5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + minimizer-webpack-plugin: 5.6.1(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)) neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 @@ -26881,14 +26839,14 @@ snapshots: - postcss - uglify-js - webpackbar@7.0.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + webpackbar@7.0.0(webpack@5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3)): dependencies: ansis: 3.17.0 consola: 3.4.2 pretty-time: 1.1.0 std-env: 3.10.0 optionalDependencies: - webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpack: 5.108.4(@swc/core@1.15.47(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.26))(csso@5.0.5)(esbuild@0.28.2)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.26)(uglify-js@3.19.3) websocket-driver@0.7.5: dependencies: diff --git a/server/Dockerfile b/server/Dockerfile index 802877421d..9d5eb1cccb 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -56,7 +56,7 @@ FROM builder AS plugins ARG TARGETPLATFORM -COPY --from=ghcr.io/jdx/mise:2026.8.1@sha256:b2297770273f71e685b8056e3b07bfda4ffc35f0fb62e0339b3cdc1e5766e2fe /usr/local/bin/mise /usr/local/bin/mise +COPY --from=ghcr.io/jdx/mise:2026.8.3@sha256:92dbc3f2573926d8974e4641ad8449f16c323130b9f41c39aff19b7b2f500ef6 /usr/local/bin/mise /usr/local/bin/mise WORKDIR /app COPY ./mise.toml ./mise.toml diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev index 23a565b4bb..9a46f41b4e 100644 --- a/server/Dockerfile.dev +++ b/server/Dockerfile.dev @@ -2,7 +2,7 @@ FROM ghcr.io/immich-app/base-server-dev:202607211135@sha256:83c9ff3f7390111596a2dcd24a746c3f8618d58259dbc7aae36618d153462f18 AS dev -COPY --from=ghcr.io/jdx/mise:2026.8.1@sha256:b2297770273f71e685b8056e3b07bfda4ffc35f0fb62e0339b3cdc1e5766e2fe /usr/local/bin/mise /usr/local/bin/mise +COPY --from=ghcr.io/jdx/mise:2026.8.3@sha256:92dbc3f2573926d8974e4641ad8449f16c323130b9f41c39aff19b7b2f500ef6 /usr/local/bin/mise /usr/local/bin/mise RUN echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc && \ echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \ diff --git a/server/eslint.config.mjs b/server/eslint.config.mjs index 2cca2cab6d..949acaf139 100644 --- a/server/eslint.config.mjs +++ b/server/eslint.config.mjs @@ -2,13 +2,8 @@ import js from '@eslint/js'; import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; import eslintPluginUnicorn from 'eslint-plugin-unicorn'; import globals from 'globals'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import typescriptEslint from 'typescript-eslint'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - export default typescriptEslint.config([ eslintPluginUnicorn.configs.recommended, eslintPluginPrettierRecommended, @@ -29,7 +24,7 @@ export default typescriptEslint.config([ parserOptions: { project: 'tsconfig.json', - tsconfigRootDir: __dirname, + tsconfigRootDir: import.meta.dirname, }, }, @@ -76,6 +71,7 @@ export default typescriptEslint.config([ curly: 2, 'prettier/prettier': 0, 'object-shorthand': ['error', 'always'], + eqeqeq: 'error', 'no-restricted-imports': [ 'error', diff --git a/server/src/controllers/activity.controller.spec.ts b/server/src/controllers/activity.controller.spec.ts index 0b677b83fa..899c7257d6 100644 --- a/server/src/controllers/activity.controller.spec.ts +++ b/server/src/controllers/activity.controller.spec.ts @@ -19,11 +19,6 @@ describe(ActivityController.name, () => { }); describe('GET /activities', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/activities'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require an albumId', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/activities'); expect(status).toEqual(400); @@ -50,11 +45,6 @@ describe(ActivityController.name, () => { }); describe('POST /activities', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/activities'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require an albumId', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/activities') @@ -77,11 +67,6 @@ describe(ActivityController.name, () => { }); describe('DELETE /activities/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/activities/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/activities/123`); expect(status).toBe(400); diff --git a/server/src/controllers/album.controller.spec.ts b/server/src/controllers/album.controller.spec.ts index 2ab7b08ceb..239a602100 100644 --- a/server/src/controllers/album.controller.spec.ts +++ b/server/src/controllers/album.controller.spec.ts @@ -19,11 +19,6 @@ describe(AlbumController.name, () => { }); describe('GET /albums', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/albums'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should reject an invalid shared param', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/albums?isShared=invalid'); expect(status).toEqual(400); @@ -40,60 +35,4 @@ describe(AlbumController.name, () => { expect(body).toEqual(factory.responses.validationError([{ path: ['assetId'], message: 'Invalid UUID' }])); }); }); - - describe('GET /albums/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/albums/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /albums/statistics', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/albums/statistics'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('POST /albums', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/albums').send({ albumName: 'New album' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('PUT /albums/:id/assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/albums/${factory.uuid()}/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('PUT /albums/assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/albums/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('PATCH /albums/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).patch(`/albums/${factory.uuid()}`).send({ albumName: 'New album name' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('DELETE /albums/:id/assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/albums/${factory.uuid()}/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('PUT :id/users', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/albums/${factory.uuid()}/users`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); }); diff --git a/server/src/controllers/api-key.controller.spec.ts b/server/src/controllers/api-key.controller.spec.ts index 91a7c43a2d..08963362b6 100644 --- a/server/src/controllers/api-key.controller.spec.ts +++ b/server/src/controllers/api-key.controller.spec.ts @@ -19,33 +19,7 @@ describe(ApiKeyController.name, () => { ctx.reset(); }); - describe('POST /api-keys', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/api-keys').send({ name: 'API Key' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /api-keys', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/api-keys'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /api-keys/me', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/api-keys/me`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('GET /api-keys/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/api-keys/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/api-keys/123`); expect(status).toBe(400); @@ -54,11 +28,6 @@ describe(ApiKeyController.name, () => { }); describe('PUT /api-keys/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/api-keys/${factory.uuid()}`).send({ name: 'new name' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()) .put(`/api-keys/123`) @@ -76,11 +45,6 @@ describe(ApiKeyController.name, () => { }); describe('DELETE /api-keys/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/api-keys/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/api-keys/123`); expect(status).toBe(400); diff --git a/server/src/controllers/asset-media.controller.spec.ts b/server/src/controllers/asset-media.controller.spec.ts index 668d88acbf..43d9a97780 100644 --- a/server/src/controllers/asset-media.controller.spec.ts +++ b/server/src/controllers/asset-media.controller.spec.ts @@ -44,11 +44,6 @@ describe(AssetMediaController.name, () => { }); describe('POST /assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post(`/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should accept metadata', async () => { const mobileMetadata = { key: AssetMetadataKey.MobileApp, value: { iCloudId: '123' } }; const { status } = await request(ctx.getHttpServer()) @@ -171,20 +166,9 @@ describe(AssetMediaController.name, () => { }); // TODO figure out how to deal with `sendFile` - describe.skip('GET /assets/:id/original', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/original`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); // TODO figure out how to deal with `sendFile` describe('GET /assets/:id/thumbnail', () => { - it.skip('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should redirect if size=original is requested', async () => { const { status } = await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail?size=original`); expect(status).toBe(302); diff --git a/server/src/controllers/asset.controller.spec.ts b/server/src/controllers/asset.controller.spec.ts index 8ee6995b1f..92710d6ffc 100644 --- a/server/src/controllers/asset.controller.spec.ts +++ b/server/src/controllers/asset.controller.spec.ts @@ -20,11 +20,6 @@ describe(AssetController.name, () => { }); describe('PUT /assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()) .put(`/assets`) @@ -59,13 +54,6 @@ describe(AssetController.name, () => { }); describe('DELETE /assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()) - .delete(`/assets`) - .send({ ids: [factory.uuid()] }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()) .delete(`/assets`) @@ -77,11 +65,6 @@ describe(AssetController.name, () => { }); describe('GET /assets/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123`); expect(status).toBe(400); @@ -90,11 +73,6 @@ describe(AssetController.name, () => { }); describe('PUT /assets/copy', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/copy`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require target and source id', async () => { const { status, body } = await request(ctx.getHttpServer()).put('/assets/copy').send({}); expect(status).toBe(400); @@ -115,11 +93,6 @@ describe(AssetController.name, () => { }); describe('PUT /assets/metadata', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/assets/metadata`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid assetId', async () => { const { status, body } = await request(ctx.getHttpServer()) .put('/assets/metadata') @@ -151,11 +124,6 @@ describe(AssetController.name, () => { }); describe('DELETE /assets/metadata', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/assets/metadata`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid assetId', async () => { const { status, body } = await request(ctx.getHttpServer()) .delete('/assets/metadata') @@ -187,11 +155,6 @@ describe(AssetController.name, () => { }); describe('PUT /assets/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/123`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123`); expect(status).toBe(400); @@ -267,26 +230,7 @@ describe(AssetController.name, () => { }); }); - describe('GET /assets/statistics', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/statistics`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /assets/:id/metadata', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('PUT /assets/:id/metadata', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({ items: [] }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123/metadata`).send({ items: [] }); expect(status).toBe(400); @@ -353,11 +297,6 @@ describe(AssetController.name, () => { }); describe('GET /assets/:id/metadata/:key', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/metadata/mobile-app`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123/metadata/mobile-app`); expect(status).toBe(400); @@ -366,11 +305,6 @@ describe(AssetController.name, () => { }); describe('PUT /assets/:id/edits', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/edits`).send({ edits: [] }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should accept valid edits and pass to service correctly', async () => { const edits = [ { @@ -456,11 +390,6 @@ describe(AssetController.name, () => { }); describe('DELETE /assets/:id/metadata/:key', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/assets/${factory.uuid()}/metadata/mobile-app`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/123/metadata/mobile-app`); expect(status).toBe(400); diff --git a/server/src/controllers/auth.controller.spec.ts b/server/src/controllers/auth.controller.spec.ts index 3bf1d59f8b..832547776d 100644 --- a/server/src/controllers/auth.controller.spec.ts +++ b/server/src/controllers/auth.controller.spec.ts @@ -199,28 +199,7 @@ describe(AuthController.name, () => { }); }); - describe('POST /auth/logout', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/auth/logout'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('POST /auth/change-password', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()) - .post('/auth/change-password') - .send({ password: 'password', newPassword: 'Password1234', invalidateSessions: false }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('POST /auth/pin-code', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '123456' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should reject 5 digits', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '12345' }); expect(status).toEqual(400); @@ -251,25 +230,4 @@ describe(AuthController.name, () => { ); }); }); - - describe('PUT /auth/pin-code', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put('/auth/pin-code').send({ pinCode: '123456', newPinCode: '654321' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('DELETE /auth/pin-code', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete('/auth/pin-code').send({ pinCode: '123456' }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /auth/status', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/auth/status'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); }); diff --git a/server/src/controllers/database-backup.controller.spec.ts b/server/src/controllers/database-backup.controller.spec.ts index 43fa779e8d..57a7ecc310 100644 --- a/server/src/controllers/database-backup.controller.spec.ts +++ b/server/src/controllers/database-backup.controller.spec.ts @@ -25,13 +25,6 @@ describe(DatabaseBackupController.name, () => { ctx.reset(); }); - describe('GET /admin/database-backups', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/admin/database-backups').send(); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('POST /admin/database-backups/start-restore', () => { it('should not be an authenticated route', async () => { maintenanceService.startRestoreFlow.mockResolvedValue({ jwt: 'jwt' }); diff --git a/server/src/controllers/download.controller.spec.ts b/server/src/controllers/download.controller.spec.ts deleted file mode 100644 index f42afe8c50..0000000000 --- a/server/src/controllers/download.controller.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Readable } from 'node:stream'; -import { DownloadController } from 'src/controllers/download.controller'; -import { DownloadService } from 'src/services/download.service'; -import request from 'supertest'; -import { factory } from 'test/small.factory'; -import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; - -describe(DownloadController.name, () => { - let ctx: ControllerContext; - const service = mockBaseService(DownloadService); - - beforeAll(async () => { - ctx = await controllerSetup(DownloadController, [{ provide: DownloadService, useValue: service }]); - return () => ctx.close(); - }); - - beforeEach(() => { - service.resetAllMocks(); - ctx.reset(); - }); - - describe('POST /download/info', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()) - .post('/download/info') - .send({ assetIds: [factory.uuid()] }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('POST /download/archive', () => { - it('should be an authenticated route', async () => { - const stream = new Readable({ - read() { - // eslint-disable-next-line unicorn/no-this-outside-of-class - this.push('test'); - // eslint-disable-next-line unicorn/no-this-outside-of-class - this.push(null); - }, - }); - service.downloadArchive.mockResolvedValue({ stream }); - await request(ctx.getHttpServer()) - .post('/download/archive') - .send({ assetIds: [factory.uuid()] }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); -}); diff --git a/server/src/controllers/duplicate.controller.spec.ts b/server/src/controllers/duplicate.controller.spec.ts index 7bbafb4665..c8ca0e5793 100644 --- a/server/src/controllers/duplicate.controller.spec.ts +++ b/server/src/controllers/duplicate.controller.spec.ts @@ -18,26 +18,7 @@ describe(DuplicateController.name, () => { ctx.reset(); }); - describe('GET /duplicates', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/duplicates'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('DELETE /duplicates', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete('/duplicates'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('DELETE /duplicates/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/duplicates/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/duplicates/123`); expect(status).toBe(400); diff --git a/server/src/controllers/maintenance.controller.spec.ts b/server/src/controllers/maintenance.controller.spec.ts index 630bb7c8b8..59b00b8aec 100644 --- a/server/src/controllers/maintenance.controller.spec.ts +++ b/server/src/controllers/maintenance.controller.spec.ts @@ -20,11 +20,6 @@ describe(MaintenanceController.name, () => { }); describe('POST /admin/maintenance', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/admin/maintenance').send(); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a backup file when action is restore', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/admin/maintenance').send({ action: MaintenanceAction.RestoreDatabase, diff --git a/server/src/controllers/memory.controller.spec.ts b/server/src/controllers/memory.controller.spec.ts index a3839793eb..b717ee966c 100644 --- a/server/src/controllers/memory.controller.spec.ts +++ b/server/src/controllers/memory.controller.spec.ts @@ -20,11 +20,6 @@ describe(MemoryController.name, () => { }); describe('GET /memories', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/memories'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should not require any parameters', async () => { await request(ctx.getHttpServer()).get('/memories').query({}); expect(service.search).toHaveBeenCalled(); @@ -32,11 +27,6 @@ describe(MemoryController.name, () => { }); describe('POST /memories', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/memories'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should validate data when type is on this day', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/memories') @@ -69,19 +59,7 @@ describe(MemoryController.name, () => { }); }); - describe('GET /memories/statistics', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/memories/statistics'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('GET /memories/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/memories/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/memories/invalid`); expect(status).toBe(400); @@ -90,11 +68,6 @@ describe(MemoryController.name, () => { }); describe('PUT /memories/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid`); expect(status).toBe(400); @@ -114,19 +87,7 @@ describe(MemoryController.name, () => { }); }); - describe('DELETE /memories/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/memories/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('PUT /memories/:id/assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid/assets`).send({ ids: [] }); expect(status).toBe(400); @@ -143,11 +104,6 @@ describe(MemoryController.name, () => { }); describe('DELETE /memories/:id/assets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/memories/${factory.uuid()}/assets`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/memories/invalid/assets`); expect(status).toBe(400); diff --git a/server/src/controllers/notification-admin.controller.spec.ts b/server/src/controllers/notification-admin.controller.spec.ts index b93726eb32..e440f5bcf6 100644 --- a/server/src/controllers/notification-admin.controller.spec.ts +++ b/server/src/controllers/notification-admin.controller.spec.ts @@ -21,11 +21,6 @@ describe(NotificationAdminController.name, () => { }); describe('POST /admin/notifications', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/admin/notifications'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should accept a null readAt', async () => { await request(ctx.getHttpServer()) .post(`/admin/notifications`) diff --git a/server/src/controllers/notification.controller.spec.ts b/server/src/controllers/notification.controller.spec.ts index 1759e13404..f65537cbba 100644 --- a/server/src/controllers/notification.controller.spec.ts +++ b/server/src/controllers/notification.controller.spec.ts @@ -20,11 +20,6 @@ describe(NotificationController.name, () => { }); describe('GET /notifications', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/notifications'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should reject an invalid notification level`, async () => { const { status, body } = await request(ctx.getHttpServer()) .get(`/notifications`) @@ -40,11 +35,6 @@ describe(NotificationController.name, () => { }); describe('PUT /notifications', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put('/notifications'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - describe('ids', () => { it('should require a list', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/notifications`).send({ ids: true }); @@ -75,11 +65,6 @@ describe(NotificationController.name, () => { }); describe('GET /notifications/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/notifications/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/notifications/123`); expect(status).toBe(400); @@ -88,11 +73,6 @@ describe(NotificationController.name, () => { }); describe('PUT /notifications/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/notifications/${factory.uuid()}`).send({ readAt: factory.date() }); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should accept a null readAt', async () => { const id = factory.uuid(); await request(ctx.getHttpServer()).put(`/notifications/${id}`).send({ readAt: null }); diff --git a/server/src/controllers/partner.controller.spec.ts b/server/src/controllers/partner.controller.spec.ts index d6541411b8..38cb717bea 100644 --- a/server/src/controllers/partner.controller.spec.ts +++ b/server/src/controllers/partner.controller.spec.ts @@ -3,7 +3,6 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; import { PartnerService } from 'src/services/partner.service'; import request from 'supertest'; import { errorDto } from 'test/medium/responses'; -import { factory } from 'test/small.factory'; import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; describe(PartnerController.name, () => { @@ -24,11 +23,6 @@ describe(PartnerController.name, () => { }); describe('GET /partners', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/partners'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require a direction`, async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/partners`).set('Authorization', `Bearer token`); expect(status).toBe(400); @@ -54,11 +48,6 @@ describe(PartnerController.name, () => { }); describe('POST /partners', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/partners'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require sharedWithId to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .post(`/partners`) @@ -70,11 +59,6 @@ describe(PartnerController.name, () => { }); describe('PUT /partners/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/partners/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .put(`/partners/invalid`) @@ -86,11 +70,6 @@ describe(PartnerController.name, () => { }); describe('DELETE /partners/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/partners/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .delete(`/partners/invalid`) diff --git a/server/src/controllers/person.controller.spec.ts b/server/src/controllers/person.controller.spec.ts index 64fe4f3554..0d3d3eea4a 100644 --- a/server/src/controllers/person.controller.spec.ts +++ b/server/src/controllers/person.controller.spec.ts @@ -24,11 +24,6 @@ describe(PersonController.name, () => { }); describe('GET /people', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/people'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require closestPersonId to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .get(`/people`) @@ -48,19 +43,7 @@ describe(PersonController.name, () => { }); }); - describe('POST /people', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/people'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('DELETE /people', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete('/people'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require uuids in the body', async () => { const { status, body } = await request(ctx.getHttpServer()) .delete('/people') @@ -78,19 +61,7 @@ describe(PersonController.name, () => { }); }); - describe('GET /people/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('PUT /people/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/people/123`); expect(status).toBe(400); @@ -177,11 +148,6 @@ describe(PersonController.name, () => { }); describe('DELETE /people/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete(`/people/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/people/invalid`); expect(status).toBe(400); @@ -194,18 +160,4 @@ describe(PersonController.name, () => { expect(service.delete).toHaveBeenCalled(); }); }); - - describe('POST /people/:id/merge', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post(`/people/${factory.uuid()}/merge`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /people/:id/statistics', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/people/${factory.uuid()}/statistics`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); }); diff --git a/server/src/controllers/plugin.controller.spec.ts b/server/src/controllers/plugin.controller.spec.ts index 881a7dd953..bc42a4f75b 100644 --- a/server/src/controllers/plugin.controller.spec.ts +++ b/server/src/controllers/plugin.controller.spec.ts @@ -3,7 +3,6 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; import { PluginService } from 'src/services/plugin.service'; import request from 'supertest'; import { errorDto } from 'test/medium/responses'; -import { factory } from 'test/small.factory'; import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; describe(PluginController.name, () => { @@ -24,11 +23,6 @@ describe(PluginController.name, () => { }); describe('GET /plugins', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/plugins'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .get(`/plugins`) @@ -40,11 +34,6 @@ describe(PluginController.name, () => { }); describe('GET /plugins/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/plugins/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .get(`/plugins/invalid`) diff --git a/server/src/controllers/search.controller.spec.ts b/server/src/controllers/search.controller.spec.ts index a1fed4c7ae..f1ac01ed41 100644 --- a/server/src/controllers/search.controller.spec.ts +++ b/server/src/controllers/search.controller.spec.ts @@ -19,11 +19,6 @@ describe(SearchController.name, () => { }); describe('POST /search/metadata', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/search/metadata'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should reject page as a string', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 'abc' }); expect(status).toBe(400); @@ -121,11 +116,6 @@ describe(SearchController.name, () => { }); describe('POST /search/random', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/search/random'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should reject if withStacked is not a boolean', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/search/random') @@ -151,26 +141,7 @@ describe(SearchController.name, () => { }); }); - describe('POST /search/smart', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/search/smart'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /search/explore', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/search/explore'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('POST /search/person', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/search/person'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a name', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({}); expect(status).toBe(400); @@ -181,11 +152,6 @@ describe(SearchController.name, () => { }); describe('GET /search/places', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/search/places'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a name', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({}); expect(status).toBe(400); @@ -195,19 +161,7 @@ describe(SearchController.name, () => { }); }); - describe('GET /search/cities', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/search/cities'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('GET /search/suggestions', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/search/suggestions'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a type', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({}); expect(status).toBe(400); diff --git a/server/src/controllers/server.controller.spec.ts b/server/src/controllers/server.controller.spec.ts deleted file mode 100644 index 6b00490d28..0000000000 --- a/server/src/controllers/server.controller.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ServerController } from 'src/controllers/server.controller'; -import { ServerService } from 'src/services/server.service'; -import { SystemMetadataService } from 'src/services/system-metadata.service'; -import { VersionService } from 'src/services/version.service'; -import request from 'supertest'; -import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; - -describe(ServerController.name, () => { - let ctx: ControllerContext; - const serverService = mockBaseService(ServerService); - const systemMetadataService = mockBaseService(SystemMetadataService); - const versionService = mockBaseService(VersionService); - - beforeAll(async () => { - ctx = await controllerSetup(ServerController, [ - { provide: ServerService, useValue: serverService }, - { provide: SystemMetadataService, useValue: systemMetadataService }, - { provide: VersionService, useValue: versionService }, - ]); - return () => ctx.close(); - }); - - beforeEach(() => { - serverService.resetAllMocks(); - versionService.resetAllMocks(); - ctx.reset(); - }); - - describe('GET /server/license', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/server/license'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); -}); diff --git a/server/src/controllers/shared-link.controller.spec.ts b/server/src/controllers/shared-link.controller.spec.ts index d8b89d0029..dc069ad267 100644 --- a/server/src/controllers/shared-link.controller.spec.ts +++ b/server/src/controllers/shared-link.controller.spec.ts @@ -2,6 +2,7 @@ import { SharedLinkController } from 'src/controllers/shared-link.controller'; import { Permission, SharedLinkType } from 'src/enum'; import { SharedLinkService } from 'src/services/shared-link.service'; import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; import { factory } from 'test/small.factory'; import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; @@ -19,10 +20,24 @@ describe(SharedLinkController.name, () => { ctx.reset(); }); + describe('GET /shared-links/me', () => { + it('should be a shared link route', async () => { + await request(ctx.getHttpServer()).get('/shared-links/me'); + expect(ctx.authenticate).toHaveBeenCalledWith( + expect.objectContaining({ metadata: expect.objectContaining({ sharedLinkRoute: true }) }), + ); + }); + }); + describe('POST /shared-links', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/shared-links'); - expect(ctx.authenticate).toHaveBeenCalled(); + it('should require a type and the correspondent asset/album id', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/shared-links') + .set('Authorization', `Bearer token`); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]), + ); }); it('should allow an null expiresAt', async () => { diff --git a/server/src/controllers/stack.controller.spec.ts b/server/src/controllers/stack.controller.spec.ts new file mode 100644 index 0000000000..bc9c87da14 --- /dev/null +++ b/server/src/controllers/stack.controller.spec.ts @@ -0,0 +1,46 @@ +import { StackController } from 'src/controllers/stack.controller'; +import { StackService } from 'src/services/stack.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { factory } from 'test/small.factory'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(StackController.name, () => { + let ctx: ControllerContext; + const service = mockBaseService(StackService); + + beforeAll(async () => { + ctx = await controllerSetup(StackController, [{ provide: StackService, useValue: service }]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('POST /stacks', () => { + it('should require at least two assets', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/stacks') + .send({ assetIds: [factory.uuid()] }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([{ path: ['assetIds'], message: 'Too small: expected array to have >=2 items' }]), + ); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post('/stacks') + .send({ assetIds: ['invalid', 'invalid'] }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([ + { path: ['assetIds', 0], message: 'Invalid UUID' }, + { path: ['assetIds', 1], message: 'Invalid UUID' }, + ]), + ); + }); + }); +}); diff --git a/server/src/controllers/sync.controller.spec.ts b/server/src/controllers/sync.controller.spec.ts index cae7650d9a..1f2c0f137a 100644 --- a/server/src/controllers/sync.controller.spec.ts +++ b/server/src/controllers/sync.controller.spec.ts @@ -25,11 +25,6 @@ describe(SyncController.name, () => { }); describe('POST /sync/stream', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/sync/stream'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require sync request type enums', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/sync/stream') @@ -44,19 +39,7 @@ describe(SyncController.name, () => { }); }); - describe('GET /sync/ack', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/sync/ack'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('POST /sync/ack', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/sync/ack'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should not allow more than 1,000 entries', async () => { const acks = Array.from({ length: 1001 }, (_, i) => `ack-${i}`); const { status, body } = await request(ctx.getHttpServer()).post('/sync/ack').send({ acks }); @@ -69,11 +52,6 @@ describe(SyncController.name, () => { }); describe('DELETE /sync/ack', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete('/sync/ack'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require sync response type enums', async () => { const { status, body } = await request(ctx.getHttpServer()) .delete('/sync/ack') diff --git a/server/src/controllers/system-config.controller.spec.ts b/server/src/controllers/system-config.controller.spec.ts index 439b5ddec1..7d40f12583 100644 --- a/server/src/controllers/system-config.controller.spec.ts +++ b/server/src/controllers/system-config.controller.spec.ts @@ -40,26 +40,7 @@ describe(SystemConfigController.name, () => { ctx.reset(); }); - describe('GET /system-config', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/system-config'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /system-config/defaults', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/system-config/defaults'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('PUT /system-config', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put('/system-config'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - describe('nightlyTasks', () => { it('should validate nightly jobs start time', async () => { const config = validConfig(); diff --git a/server/src/controllers/tag.controller.spec.ts b/server/src/controllers/tag.controller.spec.ts index c2a2de95bd..a89ce14dda 100644 --- a/server/src/controllers/tag.controller.spec.ts +++ b/server/src/controllers/tag.controller.spec.ts @@ -2,7 +2,6 @@ import { TagController } from 'src/controllers/tag.controller'; import { TagService } from 'src/services/tag.service'; import request from 'supertest'; import { errorDto } from 'test/medium/responses'; -import { factory } from 'test/small.factory'; import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; describe(TagController.name, () => { @@ -19,38 +18,14 @@ describe(TagController.name, () => { ctx.reset(); }); - describe('GET /tags', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/tags'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('POST /tags', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/tags'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should a null parentId', async () => { await request(ctx.getHttpServer()).post(`/tags`).send({ name: 'tag', parentId: null }); expect(service.create).toHaveBeenCalledWith(undefined, expect.objectContaining({ parentId: null })); }); }); - describe('PUT /tags', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put('/tags'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('GET /tags/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/tags/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/tags/123`); expect(status).toBe(400); @@ -58,10 +33,11 @@ describe(TagController.name, () => { }); }); - describe('PUT /tags/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/tags/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); + describe('DELETE /tags/:id', () => { + it('should require a valid uuid', async () => { + const { status, body } = await request(ctx.getHttpServer()).delete(`/tags/123`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); }); }); }); diff --git a/server/src/controllers/timeline.controller.spec.ts b/server/src/controllers/timeline.controller.spec.ts index ff18c020c1..288945ee97 100644 --- a/server/src/controllers/timeline.controller.spec.ts +++ b/server/src/controllers/timeline.controller.spec.ts @@ -19,11 +19,6 @@ describe(TimelineController.name, () => { }); describe('GET /timeline/buckets', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/timeline/buckets'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should parse bbox query string into an object', async () => { const { status } = await request(ctx.getHttpServer()) .get('/timeline/buckets') @@ -58,11 +53,6 @@ describe(TimelineController.name, () => { }); describe('GET /timeline/bucket', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/timeline/bucket?timeBucket=1900-01-01'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - // TODO enable date string validation while still accepting 5 digit years it.fails('should fail if time bucket is invalid', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/timeline/bucket').query({ timeBucket: 'foo' }); diff --git a/server/src/controllers/user-admin.controller.spec.ts b/server/src/controllers/user-admin.controller.spec.ts index b5840a33e1..3fa423f9b3 100644 --- a/server/src/controllers/user-admin.controller.spec.ts +++ b/server/src/controllers/user-admin.controller.spec.ts @@ -24,26 +24,7 @@ describe(UserAdminController.name, () => { ctx.reset(); }); - describe('GET /admin/users', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/admin/users'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('PUT /admin/users/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/admin/users/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('POST /admin/users', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/admin/users'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it('should allow a null pinCode', async () => { await request(ctx.getHttpServer()).post(`/admin/users`).send({ name: 'Test user', @@ -64,25 +45,22 @@ describe(UserAdminController.name, () => { expect(service.create).toHaveBeenCalledWith(expect.objectContaining({ avatarColor: null })); }); - it(`should `, async () => { - const dto: UserAdminCreateDto = { - email: 'user@immich.app', - password: 'test', - name: 'Test User', - quotaSizeInBytes: 1.2, - }; - - const { status, body } = await request(ctx.getHttpServer()) - .post(`/admin/users`) - .set('Authorization', `Bearer token`) - .send(dto); - expect(status).toBe(400); - expect(body).toEqual( - errorDto.validationError([ - { path: ['quotaSizeInBytes'], message: 'Invalid input: expected int, received number' }, - ]), - ); - }); + for (const [key, message] of [ + ['password', 'Invalid input: expected string, received null'], + ['email', 'Invalid input: expected email, received object'], + ['name', 'Invalid input: expected string, received null'], + ['shouldChangePassword', 'Invalid input: expected boolean, received null'], + ['notify', 'Invalid input: expected boolean, received null'], + ] as const) { + it(`should not allow null ${key}`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .post(`/admin/users`) + .set('Authorization', `Bearer token`) + .send({ email: 'user@immich.app', password: 'test', name: 'Test User', [key]: null }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.validationError([{ path: [key], message }])); + }); + } it(`should not allow decimal quota`, async () => { const dto: UserAdminCreateDto = { @@ -105,19 +83,7 @@ describe(UserAdminController.name, () => { }); }); - describe('GET /admin/users/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/admin/users/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('PUT /admin/users/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put(`/admin/users/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should not allow decimal quota`, async () => { const { status, body } = await request(ctx.getHttpServer()) .put(`/admin/users/${factory.uuid()}`) @@ -142,5 +108,21 @@ describe(UserAdminController.name, () => { await request(ctx.getHttpServer()).put(`/admin/users/${id}`).send({ avatarColor: null }); expect(service.update).toHaveBeenCalledWith(undefined, id, expect.objectContaining({ avatarColor: null })); }); + + for (const [key, message] of [ + ['password', 'Invalid input: expected string, received null'], + ['email', 'Invalid input: expected email, received object'], + ['name', 'Invalid input: expected string, received null'], + ['shouldChangePassword', 'Invalid input: expected boolean, received null'], + ] as const) { + it(`should not allow null ${key}`, async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/admin/users/${factory.uuid()}`) + .set('Authorization', `Bearer token`) + .send({ [key]: null }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.validationError([{ path: [key], message }])); + }); + } }); }); diff --git a/server/src/controllers/user.controller.spec.ts b/server/src/controllers/user.controller.spec.ts index f512e2de39..d15eda31dc 100644 --- a/server/src/controllers/user.controller.spec.ts +++ b/server/src/controllers/user.controller.spec.ts @@ -3,7 +3,6 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; import { UserService } from 'src/services/user.service'; import request from 'supertest'; import { errorDto } from 'test/medium/responses'; -import { factory } from 'test/small.factory'; import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; describe(UserController.name, () => { @@ -23,26 +22,7 @@ describe(UserController.name, () => { ctx.reset(); }); - describe('GET /users', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/users'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('GET /users/me', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/users/me'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - describe('PUT /users/me', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put('/users/me'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - for (const [key, message] of [ ['email', 'Invalid input: expected email, received object'], ['name', 'Invalid input: expected string, received null'], @@ -66,24 +46,31 @@ describe(UserController.name, () => { }); }); - describe('GET /users/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/users/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); + describe('PUT /users/me/preferences', () => { + it('should require an integer for download archive size', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/users/me/preferences`) + .set('Authorization', `Bearer token`) + .send({ download: { archiveSize: 1_234_567.89 } }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([ + { path: ['download', 'archiveSize'], message: 'Invalid input: expected int, received number' }, + ]), + ); }); - }); - describe('PUT /users/me/license', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).put('/users/me/license'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - }); - - describe('DELETE /users/me/license', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).delete('/users/me/license'); - expect(ctx.authenticate).toHaveBeenCalled(); + it('should require a boolean for download include embedded videos', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .put(`/users/me/preferences`) + .set('Authorization', `Bearer token`) + .send({ download: { includeEmbeddedVideos: 1_234_567.89 } }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([ + { path: ['download', 'includeEmbeddedVideos'], message: 'Invalid input: expected boolean, received number' }, + ]), + ); }); }); }); diff --git a/server/src/controllers/workflow.controller.spec.ts b/server/src/controllers/workflow.controller.spec.ts index ad0dc4982d..0b91667671 100644 --- a/server/src/controllers/workflow.controller.spec.ts +++ b/server/src/controllers/workflow.controller.spec.ts @@ -4,7 +4,6 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; import { WorkflowService } from 'src/services/workflow.service'; import request from 'supertest'; import { errorDto } from 'test/medium/responses'; -import { factory } from 'test/small.factory'; import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; describe(WorkflowController.name, () => { @@ -25,11 +24,6 @@ describe(WorkflowController.name, () => { }); describe('POST /workflows', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).post('/workflows').send({}); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require a valid trigger`, async () => { const { status, body } = await request(ctx.getHttpServer()) .post(`/workflows`) @@ -65,11 +59,6 @@ describe(WorkflowController.name, () => { }); describe('GET /workflows', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get('/workflows'); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .get(`/workflows`) @@ -81,11 +70,6 @@ describe(WorkflowController.name, () => { }); describe('GET /workflows/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).get(`/workflows/${factory.uuid()}`); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .get(`/workflows/invalid`) @@ -96,11 +80,6 @@ describe(WorkflowController.name, () => { }); describe('PATCH /workflows/:id', () => { - it('should be an authenticated route', async () => { - await request(ctx.getHttpServer()).patch(`/workflows/${factory.uuid()}`).send({}); - expect(ctx.authenticate).toHaveBeenCalled(); - }); - it(`should require id to be a uuid`, async () => { const { status, body } = await request(ctx.getHttpServer()) .patch(`/workflows/invalid`) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index ca00245c27..add3f0a24d 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -838,7 +838,7 @@ export class AssetRepository { ) .$if(!!options.withCoordinates, (qb) => qb.select(['asset_exif.latitude', 'asset_exif.longitude'])) .where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null) - .$if(options.visibility == undefined, withDefaultVisibility) + .$if(options.visibility === undefined, withDefaultVisibility) .$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!)) .$if(!!options.bbox, (qb) => { const bbox = options.bbox!; @@ -899,7 +899,7 @@ export class AssetRepository { .$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted)) .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)) .orderBy( - options.orderBy == AssetOrderBy.CreatedAt + options.orderBy === AssetOrderBy.CreatedAt ? sql`"createdAt"` : sql`(asset."localDateTime" AT TIME ZONE 'UTC')::date`, order, diff --git a/server/src/repositories/telemetry.repository.ts b/server/src/repositories/telemetry.repository.ts index 9a486005b5..146eaa30d7 100644 --- a/server/src/repositories/telemetry.repository.ts +++ b/server/src/repositories/telemetry.repository.ts @@ -149,7 +149,7 @@ export class TelemetryRepository { const unit = 'ms'; for (const [propName, descriptor] of Object.entries(descriptors)) { - const isMethod = typeof descriptor.value == 'function' && propName !== 'constructor'; + const isMethod = typeof descriptor.value === 'function' && propName !== 'constructor'; if (!isMethod) { continue; } diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index 11a6e532aa..afe14fb499 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -321,7 +321,7 @@ export class UserRepository { updatedAt: new Date(), }) .where('user.deletedAt', 'is', null) - .$if(id != undefined, (eb) => eb.where('user.id', '=', asUuid(id!))); + .$if(id !== undefined, (eb) => eb.where('user.id', '=', asUuid(id!))); await query.execute(); } diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index 230f22478d..2ee61d82dd 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -133,6 +133,20 @@ export const album_asset_delete_audit = registerFunction({ END`, }); +export const album_user_delete = registerFunction({ + name: 'album_user_delete', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + DELETE FROM "album" + WHERE "album"."id" = OLD."albumId" + AND NOT EXISTS (SELECT "albumId" FROM "album_user" WHERE "album_user"."albumId" = "album"."id" AND "album_user"."role" = 'owner'); + + RETURN NULL; + END`, +}); + export const album_user_delete_audit = registerFunction({ name: 'album_user_delete_audit', returnType: 'TRIGGER', diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index cfd1c24084..af070ba822 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -7,6 +7,7 @@ import { } from 'src/schema/enums'; import { album_user_after_insert, + album_user_delete, album_user_delete_audit, asset_delete_audit, asset_face_audit, @@ -175,6 +176,7 @@ export class ImmichDatabase { asset_metadata_audit, asset_face_audit, asset_ocr_delete_audit, + album_user_delete, ]; enum = [album_user_role_enum, assets_status_enum, asset_face_source_type, asset_visibility_enum]; diff --git a/server/src/schema/migrations/1786385711807-AlbumOwnerDeleteTrigger.ts b/server/src/schema/migrations/1786385711807-AlbumOwnerDeleteTrigger.ts new file mode 100644 index 0000000000..082e43ff23 --- /dev/null +++ b/server/src/schema/migrations/1786385711807-AlbumOwnerDeleteTrigger.ts @@ -0,0 +1,31 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`DELETE FROM "album" WHERE NOT EXISTS (SELECT * FROM "album_user" WHERE "album_user"."albumId" = "album"."id" AND "album_user"."role" = 'owner');`.execute(db); + await sql`CREATE OR REPLACE FUNCTION album_user_delete() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + DELETE FROM "album" + WHERE "album"."id" = OLD."albumId" + AND NOT EXISTS (SELECT "albumId" FROM "album_user" WHERE "album_user"."albumId" = "album"."id" AND "album_user"."role" = 'owner'); + + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "album_user_delete" + AFTER DELETE ON "album_user" + REFERENCING OLD TABLE AS "old" + FOR EACH ROW + EXECUTE FUNCTION album_user_delete();`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_album_user_delete', '{"type":"function","name":"album_user_delete","sql":"CREATE OR REPLACE FUNCTION album_user_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n DELETE FROM \\"album\\"\\n WHERE \\"album\\".\\"id\\" = OLD.\\"albumId\\"\\n AND NOT EXISTS (SELECT \\"albumId\\" FROM \\"album_user\\" WHERE \\"album_user\\".\\"albumId\\" = \\"album\\".\\"id\\" AND \\"album_user\\".\\"role\\" = ''owner'');\\n\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_album_user_delete', '{"type":"trigger","name":"album_user_delete","sql":"CREATE OR REPLACE TRIGGER \\"album_user_delete\\"\\n AFTER DELETE ON \\"album_user\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION album_user_delete();"}'::jsonb);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TRIGGER "album_user_delete" ON "album_user";`.execute(db); + await sql`DROP FUNCTION album_user_delete;`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_album_user_delete';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_album_user_delete';`.execute(db); +} diff --git a/server/src/schema/tables/album-user.table.ts b/server/src/schema/tables/album-user.table.ts index 677d6ca2f2..c015c1354d 100644 --- a/server/src/schema/tables/album-user.table.ts +++ b/server/src/schema/tables/album-user.table.ts @@ -13,7 +13,7 @@ import { import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { AlbumUserRole } from 'src/enum'; import { album_user_role_enum } from 'src/schema/enums'; -import { album_user_after_insert, album_user_delete_audit } from 'src/schema/functions'; +import { album_user_after_insert, album_user_delete, album_user_delete_audit } from 'src/schema/functions'; import { AlbumTable } from 'src/schema/tables/album.table'; import { UserTable } from 'src/schema/tables/user.table'; @@ -38,6 +38,7 @@ import { UserTable } from 'src/schema/tables/user.table'; referencingOldTableAs: 'old', when: 'pg_trigger_depth() <= 1', }) +@AfterDeleteTrigger({ scope: 'row', function: album_user_delete, referencingOldTableAs: 'old' }) export class AlbumUserTable { @ForeignKeyColumn(() => AlbumTable, { onDelete: 'CASCADE', diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 55e3682e38..a9577f4f68 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -19,6 +19,7 @@ import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.reposi import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; import { asDateTimeString } from 'src/utils/date'; +import { findOrFail } from 'src/utils/misc'; import { getPreferences } from 'src/utils/preferences'; @Injectable() @@ -353,11 +354,7 @@ export class AlbumService extends BaseService { await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role }); } - private async findOrFail(id: string, authUserId: string, options: AlbumInfoOptions) { - const album = await this.albumRepository.getById(id, options, authUserId); - if (!album) { - throw new BadRequestException('Album not found'); - } - return album; + private findOrFail(id: string, authUserId: string, options: AlbumInfoOptions) { + return findOrFail(() => this.albumRepository.getById(id, options, authUserId), 'Album'); } } diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 1edccf9483..15385df0e0 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import _ from 'lodash'; import { DateTime, Duration } from 'luxon'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { AssetFile } from 'src/database'; import { OnJob } from 'src/decorators'; import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; @@ -46,6 +45,7 @@ import { } from 'src/utils/asset.util'; import { updateLockedColumns } from 'src/utils/database'; import { extractTimeZone } from 'src/utils/date'; +import { batched, findOrFail } from 'src/utils/misc'; import { transformOcrBoundingBox } from 'src/utils/transform'; @Injectable() @@ -278,31 +278,12 @@ export class AssetService extends BaseService { .minus(Duration.fromObject({ days: trashedDays })) .toJSDate(); - let chunk: Array<{ id: string; isOffline: boolean }> = []; - const queueChunk = async () => { - if (chunk.length === 0) { - return; - } - + for await (const assets of batched(this.assetJobRepository.streamForDeletedJob(trashedBefore))) { await this.jobRepository.queueAll( - chunk.map(({ id, isOffline }) => ({ - name: JobName.AssetDelete, - data: { id, deleteOnDisk: !isOffline }, - })), + assets.map(({ id, isOffline }) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: !isOffline } })), ); - chunk = []; - }; - - const assets = this.assetJobRepository.streamForDeletedJob(trashedBefore); - for await (const asset of assets) { - chunk.push(asset); - if (chunk.length >= JOBS_ASSET_PAGINATION_SIZE) { - await queueChunk(); - } } - await queueChunk(); - return JobStatus.Success; } @@ -496,12 +477,8 @@ export class AssetService extends BaseService { await this.jobRepository.queueAll(jobs); } - private async findOrFail(id: string) { - const asset = await this.assetRepository.getById(id); - if (!asset) { - throw new BadRequestException('Asset not found'); - } - return asset; + private findOrFail(id: string) { + return findOrFail(() => this.assetRepository.getById(id), 'Asset'); } private async updateExif(dto: { diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 59e20276af..5603819212 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -549,7 +549,7 @@ export class AuthService extends BaseService { const now = DateTime.now(); const updatedAt = DateTime.fromJSDate(session.updatedAt); const diff = now.diff(updatedAt, ['hours']); - if (diff.hours > 1 || appVersion != session.appVersion) { + if (diff.hours > 1 || appVersion !== session.appVersion) { await this.sessionRepository.update(session.id, { id: session.id, updatedAt: new Date(), diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index 8cb04e29ba..08c7630f91 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@nestjs/common'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { OnJob } from 'src/decorators'; import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; @@ -8,9 +7,9 @@ import { DuplicateResolveDto, DuplicateResolveGroupDto, DuplicateResponseDto } f import { AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum'; import { AssetDuplicateResult } from 'src/repositories/search.repository'; import { BaseService } from 'src/services/base.service'; -import { JobItem, JobOf } from 'src/types'; +import { JobOf } from 'src/types'; import { suggestDuplicateKeepAssetIds } from 'src/utils/duplicate'; -import { isDuplicateDetectionEnabled } from 'src/utils/misc'; +import { batched, isDuplicateDetectionEnabled } from 'src/utils/misc'; type ResolveRequest = { assetUpdate: { @@ -307,22 +306,12 @@ export class DuplicateService extends BaseService { return JobStatus.Skipped; } - let jobs: JobItem[] = []; - const queueAll = async () => { - await this.jobRepository.queueAll(jobs); - jobs = []; - }; - - const assets = this.assetJobRepository.streamForSearchDuplicates(force); - for await (const asset of assets) { - jobs.push({ name: JobName.AssetDetectDuplicates, data: { id: asset.id } }); - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await queueAll(); - } + for await (const assets of batched(this.assetJobRepository.streamForSearchDuplicates(force))) { + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.AssetDetectDuplicates, data: { id: asset.id } })), + ); } - await queueAll(); - return JobStatus.Success; } diff --git a/server/src/services/integrity.service.ts b/server/src/services/integrity.service.ts index a7a194b25f..6c7a505812 100644 --- a/server/src/services/integrity.service.ts +++ b/server/src/services/integrity.service.ts @@ -35,7 +35,7 @@ import { IIntegrityUntrackedFilesJob, } from 'src/types'; import { ImmichFileResponse } from 'src/utils/file'; -import { handlePromiseError } from 'src/utils/misc'; +import { batched, handlePromiseError } from 'src/utils/misc'; /** * Untracked Files: @@ -201,7 +201,7 @@ export class IntegrityService extends BaseService { const reports = this.integrityRepository.streamIntegrityReportsWithAssetChecksum(IntegrityReport.UntrackedFile); let total = 0; - for await (const batchReports of chunk(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { + for await (const batchReports of batched(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { await this.jobRepository.queue({ name: JobName.IntegrityUntrackedFilesRefresh, data: { @@ -338,7 +338,7 @@ export class IntegrityService extends BaseService { const reports = this.integrityRepository.streamIntegrityReportsWithAssetChecksum(IntegrityReport.MissingFile); let total = 0; - for await (const batchReports of chunk(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { + for await (const batchReports of batched(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { await this.jobRepository.queue({ name: JobName.IntegrityMissingFilesRefresh, data: { @@ -365,7 +365,7 @@ export class IntegrityService extends BaseService { const assetPaths = this.integrityRepository.streamAssetPathsForMissingFiles(); let total = 0; - for await (const batchPaths of chunk(assetPaths, JOBS_LIBRARY_PAGINATION_SIZE)) { + for await (const batchPaths of batched(assetPaths, JOBS_LIBRARY_PAGINATION_SIZE)) { await this.jobRepository.queue({ name: JobName.IntegrityMissingFiles, data: { @@ -450,7 +450,7 @@ export class IntegrityService extends BaseService { const reports = this.integrityRepository.streamIntegrityReportsWithAssetChecksum(IntegrityReport.ChecksumFail); let total = 0; - for await (const batchReports of chunk(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { + for await (const batchReports of batched(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { await this.jobRepository.queue({ name: JobName.IntegrityChecksumFilesRefresh, data: { @@ -656,7 +656,7 @@ export class IntegrityService extends BaseService { for (const property of properties) { const reports = this.integrityRepository.streamIntegrityReportsByProperty(property, type); - for await (const batch of chunk(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { + for await (const batch of batched(reports, JOBS_LIBRARY_PAGINATION_SIZE)) { await this.jobRepository.queue({ name: JobName.IntegrityDeleteReports, data: { @@ -705,19 +705,3 @@ export class IntegrityService extends BaseService { return JobStatus.Success; } } - -async function* chunk(generator: AsyncIterableIterator, n: number) { - let chunk: T[] = []; - for await (const item of generator) { - chunk.push(item); - - if (chunk.length === n) { - yield chunk; - chunk = []; - } - } - - if (chunk.length > 0) { - yield chunk; - } -} diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index f13a440f84..9e5e0cf4d1 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -34,7 +34,7 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { BaseService } from 'src/services/base.service'; import { JobOf } from 'src/types'; import { mimeTypes } from 'src/utils/mime-types'; -import { handlePromiseError } from 'src/utils/misc'; +import { batched, findOrFail, handlePromiseError } from 'src/utils/misc'; @Injectable() export class LibraryService extends BaseService { @@ -375,35 +375,20 @@ export class LibraryService extends BaseService { await this.assetRepository.updateByLibraryId(libraryId, { deletedAt: new Date() }); - let isAssetsFound = false; - let chunk: string[] = []; - - const queueChunk = async () => { - if (chunk.length === 0) { - return; - } - - isAssetsFound = true; - this.logger.debug(`Queueing deletion of ${chunk.length} asset(s) in library ${libraryId}`); - await this.jobRepository.queueAll( - chunk.map((id) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: false } })), - ); - chunk = []; - }; - this.logger.debug(`Will delete all assets in library ${libraryId}`); - const assets = this.libraryRepository.streamAssetIds(libraryId); - for await (const asset of assets) { - chunk.push(asset.id); - - if (chunk.length >= JOBS_LIBRARY_PAGINATION_SIZE) { - await queueChunk(); - } + let hasAssets = false; + for await (const assets of batched( + this.libraryRepository.streamAssetIds(libraryId), + JOBS_LIBRARY_PAGINATION_SIZE, + )) { + this.logger.debug(`Queueing deletion of ${assets.length} asset(s) in library ${libraryId}`); + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.AssetDelete, data: { id: asset.id, deleteOnDisk: false } })), + ); + hasAssets = true; } - await queueChunk(); - - if (!isAssetsFound) { + if (!hasAssets) { this.logger.log(`Deleting library ${libraryId}`); await this.libraryRepository.delete(libraryId); } @@ -746,15 +731,12 @@ export class LibraryService extends BaseService { return JobStatus.Success; } - let chunk: string[] = []; + this.logger.log(`Scanning library ${library.id} for assets missing from disk...`); + let count = 0; - - const queueChunk = async () => { - if (chunk.length === 0) { - return; - } - - count += chunk.length; + const existingAssets = this.libraryRepository.streamAssetIds(library.id); + for await (const assets of batched(existingAssets, JOBS_LIBRARY_PAGINATION_SIZE)) { + count += assets.length; await this.jobRepository.queue({ name: JobName.LibrarySyncAssets, @@ -762,42 +744,25 @@ export class LibraryService extends BaseService { libraryId: library.id, importPaths: library.importPaths, exclusionPatterns: library.exclusionPatterns, - assetIds: chunk.map((id) => id), + assetIds: assets.map(({ id }) => id), progressCounter: count, totalAssets: assetCount, }, }); - chunk = []; const completePercentage = ((100 * count) / assetCount).toFixed(1); this.logger.log( `Queued check of ${count} of ${assetCount} (${completePercentage} %) existing asset(s) so far in library ${library.id}`, ); - }; - - this.logger.log(`Scanning library ${library.id} for assets missing from disk...`); - const existingAssets = this.libraryRepository.streamAssetIds(library.id); - - for await (const asset of existingAssets) { - chunk.push(asset.id); - if (chunk.length === JOBS_LIBRARY_PAGINATION_SIZE) { - await queueChunk(); - } } - await queueChunk(); - this.logger.log(`Finished queuing ${count} asset check(s) for library ${library.id}`); return JobStatus.Success; } - private async findOrFail(id: string) { - const library = await this.libraryRepository.get(id); - if (!library) { - throw new BadRequestException('Library not found'); - } - return library; + private findOrFail(id: string) { + return findOrFail(() => this.libraryRepository.get(id), 'Library'); } } diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 290bf947a1..d64e0088eb 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -207,8 +207,7 @@ describe(MediaService.name, () => { await sut.handleQueueGenerateThumbnails({ force: false }); expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([]); - + expect(mocks.job.queueAll).toHaveBeenCalledTimes(1); expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); }); @@ -237,7 +236,10 @@ describe(MediaService.name, () => { await sut.handleQueueGenerateThumbnails({ force: false }); expect(mocks.assetJob.streamForThumbnailJob).toHaveBeenCalledWith({ force: false, fullsizeEnabled: false }); - expect(mocks.job.queueAll).toHaveBeenCalledWith([]); + expect(mocks.job.queueAll).toHaveBeenCalledTimes(1); + expect(mocks.job.queueAll).toHaveBeenCalledWith([ + { name: JobName.AssetEditThumbnailGeneration, data: { id: asset.id } }, + ]); expect(mocks.person.getAll).toHaveBeenCalledWith({ thumbnailPath: '' }); }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 956636dd51..19b1688e88 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { SystemConfig } from 'src/config'; -import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; +import { FACE_THUMBNAIL_SIZE } from 'src/constants'; import { ImagePathOptions, StorageCore, ThumbnailPathEntity } from 'src/cores/storage.core'; import { AssetFile } from 'src/database'; import { OnEvent, OnJob } from 'src/decorators'; @@ -43,7 +43,7 @@ import { getAssetFile, getDimensions } from 'src/utils/asset.util'; import { checkFaceVisibility, checkOcrVisibility } from 'src/utils/editor'; import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; import { mimeTypes } from 'src/utils/mime-types'; -import { clamp } from 'src/utils/misc'; +import { batched, clamp } from 'src/utils/misc'; import { getOutputDimensions } from 'src/utils/transform'; interface UpsertFileOptions { @@ -69,52 +69,42 @@ export class MediaService extends BaseService { @OnJob({ name: JobName.AssetGenerateThumbnailsQueueAll, queue: QueueName.ThumbnailGeneration }) async handleQueueGenerateThumbnails({ force }: JobOf): Promise { const config = await this.getConfig({ withCache: true }); - let jobs: JobItem[] = []; - - const queueAll = async () => { - await this.jobRepository.queueAll(jobs); - jobs = []; - }; const isFullsizeEnabled = config.image.fullsize.enabled; - for await (const asset of this.assetJobRepository.streamForThumbnailJob({ - force, - fullsizeEnabled: isFullsizeEnabled, - })) { - if (force || !asset.isEdited) { - jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } }); - } - - if (asset.isEdited) { - jobs.push({ name: JobName.AssetEditThumbnailGeneration, data: { id: asset.id } }); - } - - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await queueAll(); - } - } - - await queueAll(); - - const people = this.personRepository.getAll(force ? undefined : { thumbnailPath: '' }); - - for await (const person of people) { - if (!person.faceAssetId) { - const face = await this.personRepository.getRandomFace(person.id); - if (!face) { - continue; + for await (const assets of batched( + this.assetJobRepository.streamForThumbnailJob({ force, fullsizeEnabled: isFullsizeEnabled }), + )) { + const jobs: JobItem[] = []; + for (const asset of assets) { + if (force || !asset.isEdited) { + jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } }); } - await this.personRepository.update({ id: person.id, faceAssetId: face.id }); + if (asset.isEdited) { + jobs.push({ name: JobName.AssetEditThumbnailGeneration, data: { id: asset.id } }); + } } - jobs.push({ name: JobName.PersonGenerateThumbnail, data: { id: person.id } }); - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await queueAll(); - } + await this.jobRepository.queueAll(jobs); } - await queueAll(); + for await (const people of batched(this.personRepository.getAll(force ? undefined : { thumbnailPath: '' }))) { + const jobs: JobItem[] = []; + for (const person of people) { + if (!person.faceAssetId) { + const face = await this.personRepository.getRandomFace(person.id); + if (!face) { + continue; + } + + await this.personRepository.update({ id: person.id, faceAssetId: face.id }); + } + + jobs.push({ name: JobName.PersonGenerateThumbnail, data: { id: person.id } }); + } + + await this.jobRepository.queueAll(jobs); + } return JobStatus.Success; } @@ -127,30 +117,18 @@ export class MediaService extends BaseService { await this.storageCore.removeEmptyDirs(StorageFolder.EncodedVideo); } - let jobs: JobItem[] = []; - const assets = this.assetJobRepository.streamForMigrationJob(); - for await (const asset of assets) { - jobs.push({ name: JobName.AssetFileMigration, data: { id: asset.id } }); - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(jobs); - jobs = []; - } + for await (const assets of batched(this.assetJobRepository.streamForMigrationJob())) { + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.AssetFileMigration, data: { id: asset.id } })), + ); } - await this.jobRepository.queueAll(jobs); - jobs = []; - - for await (const person of this.personRepository.getAll()) { - jobs.push({ name: JobName.PersonFileMigration, data: { id: person.id } }); - - if (jobs.length === JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(jobs); - jobs = []; - } + for await (const people of batched(this.personRepository.getAll())) { + await this.jobRepository.queueAll( + people.map((person) => ({ name: JobName.PersonFileMigration, data: { id: person.id } })), + ); } - await this.jobRepository.queueAll(jobs); - return JobStatus.Success; } @@ -551,18 +529,12 @@ export class MediaService extends BaseService { async handleQueueVideoConversion(job: JobOf): Promise { const { force } = job; - let queue: { name: JobName.AssetEncodeVideo; data: { id: string } }[] = []; - for await (const asset of this.assetJobRepository.streamForVideoConversion(force)) { - queue.push({ name: JobName.AssetEncodeVideo, data: { id: asset.id } }); - - if (queue.length >= JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(queue); - queue = []; - } + for await (const assets of batched(this.assetJobRepository.streamForVideoConversion(force))) { + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.AssetEncodeVideo, data: { id: asset.id } })), + ); } - await this.jobRepository.queueAll(queue); - return JobStatus.Success; } diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 41510056ff..1175a11af4 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { Memory } from 'src/database'; import { OnJob } from 'src/decorators'; @@ -8,6 +8,7 @@ import { MemoryCreateDto, MemoryResponseDto, MemorySearchDto, MemoryUpdateDto, m import { DatabaseLock, JobName, MemoryType, Permission, QueueName, SystemMetadataKey } from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; +import { findOrFail } from 'src/utils/misc'; const DAYS = 3; @@ -162,11 +163,7 @@ export class MemoryService extends BaseService { return results; } - private async findOrFail(id: string) { - const memory = await this.memoryRepository.get(id); - if (!memory) { - throw new BadRequestException('Memory not found'); - } - return memory; + private findOrFail(id: string) { + return findOrFail(() => this.memoryRepository.get(id), 'Memory'); } } diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 37dd92e27d..a95d1f1497 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -6,7 +6,7 @@ import { DateTime, Duration } from 'luxon'; import { Stats } from 'node:fs'; import { constants } from 'node:fs/promises'; import { join, parse } from 'node:path'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; + import { StorageCore } from 'src/cores/storage.core'; import { Asset, AssetFile } from 'src/database'; import { OnEvent, OnJob } from 'src/decorators'; @@ -30,12 +30,12 @@ import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { PersonTable } from 'src/schema/tables/person.table'; import { BaseService } from 'src/services/base.service'; -import { JobItem, JobOf } from 'src/types'; +import { JobOf } from 'src/types'; import { getAssetFiles } from 'src/utils/asset.util'; import { isAssetChecksumConstraint } from 'src/utils/database'; import { mergeTimeZone } from 'src/utils/date'; import { mimeTypes } from 'src/utils/mime-types'; -import { isFaceImportEnabled } from 'src/utils/misc'; +import { batched, isFaceImportEnabled } from 'src/utils/misc'; import { upsertTags } from 'src/utils/tag'; import { Tasks } from 'src/utils/tasks'; @@ -109,7 +109,7 @@ const validateRange = (value: number | undefined, min: number, max: number): Non const val = validate(value); // check if the value is within the range - if (val == null || val < min || val > max) { + if (val === null || val < min || val > max) { return null; } @@ -218,17 +218,12 @@ export class MetadataService extends BaseService { async handleQueueMetadataExtraction(job: JobOf): Promise { const { force } = job; - let queue: { name: JobName.AssetExtractMetadata; data: { id: string } }[] = []; - for await (const asset of this.assetJobRepository.streamForMetadataExtraction(force)) { - queue.push({ name: JobName.AssetExtractMetadata, data: { id: asset.id } }); - - if (queue.length >= JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(queue); - queue = []; - } + for await (const assets of batched(this.assetJobRepository.streamForMetadataExtraction(force))) { + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.AssetExtractMetadata, data: { id: asset.id } })), + ); } - await this.jobRepository.queueAll(queue); return JobStatus.Success; } @@ -377,8 +372,8 @@ export class MetadataService extends BaseService { fileModifiedAt: stats.mtime, // Keep unedited assets in sync with the file on disk, but don't overwrite edited dimensions. - width: !asset.isEdited || asset.width == null ? assetWidth : undefined, - height: !asset.isEdited || asset.height == null ? assetHeight : undefined, + width: !asset.isEdited || asset.width === null ? assetWidth : undefined, + height: !asset.isEdited || asset.height === null ? assetHeight : undefined, }), async () => { await this.assetRepository.upsertExif({ @@ -417,22 +412,12 @@ export class MetadataService extends BaseService { @OnJob({ name: JobName.SidecarQueueAll, queue: QueueName.Sidecar }) async handleQueueSidecar({ force }: JobOf): Promise { - let jobs: JobItem[] = []; - const queueAll = async () => { - await this.jobRepository.queueAll(jobs); - jobs = []; - }; - - const assets = this.assetJobRepository.streamForSidecar(force); - for await (const asset of assets) { - jobs.push({ name: JobName.SidecarCheck, data: { id: asset.id } }); - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await queueAll(); - } + for await (const assets of batched(this.assetJobRepository.streamForSidecar(force))) { + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.SidecarCheck, data: { id: asset.id } })), + ); } - await queueAll(); - return JobStatus.Success; } @@ -1003,7 +988,7 @@ export class MetadataService extends BaseService { // timezone let timeZone = exifTags.zone ?? null; - if (timeZone == null && (dateTime?.rawValue?.endsWith('Z') || dateTime?.rawValue?.endsWith('+00:00'))) { + if (timeZone === null && (dateTime?.rawValue?.endsWith('Z') || dateTime?.rawValue?.endsWith('+00:00'))) { // exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly // https://github.com/photostructure/exiftool-vendored.js/issues/203 timeZone = 'UTC+0'; diff --git a/server/src/services/ocr.service.ts b/server/src/services/ocr.service.ts index d92d399dba..8e32a44376 100644 --- a/server/src/services/ocr.service.ts +++ b/server/src/services/ocr.service.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; + import { OnJob } from 'src/decorators'; import { AssetVisibility, JobName, JobStatus, QueueName } from 'src/enum'; import { OCR } from 'src/repositories/machine-learning.repository'; import { BaseService } from 'src/services/base.service'; -import { JobItem, JobOf } from 'src/types'; +import { JobOf } from 'src/types'; import { tokenizeForSearch } from 'src/utils/database'; -import { isOcrEnabled } from 'src/utils/misc'; +import { batched, isOcrEnabled } from 'src/utils/misc'; @Injectable() export class OcrService extends BaseService { @@ -21,19 +21,10 @@ export class OcrService extends BaseService { await this.ocrRepository.deleteAll(); } - let jobs: JobItem[] = []; - const assets = this.assetJobRepository.streamForOcrJob(force); - - for await (const asset of assets) { - jobs.push({ name: JobName.Ocr, data: { id: asset.id } }); - - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(jobs); - jobs = []; - } + for await (const assets of batched(this.assetJobRepository.streamForOcrJob(force))) { + await this.jobRepository.queueAll(assets.map((asset) => ({ name: JobName.Ocr, data: { id: asset.id } }))); } - await this.jobRepository.queueAll(jobs); return JobStatus.Success; } diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 8e0cd2ff01..7c08c36e17 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; import { Insertable, Updateable } from 'kysely'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { Person } from 'src/database'; import { Chunked, OnJob } from 'src/decorators'; import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; @@ -43,7 +42,7 @@ import { JobItem, JobOf } from 'src/types'; import { getDimensions } from 'src/utils/asset.util'; import { ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; -import { isFacialRecognitionEnabled } from 'src/utils/misc'; +import { batched, findOrFail, isFacialRecognitionEnabled } from 'src/utils/misc'; import { Point, transformPoints } from 'src/utils/transform'; @Injectable() @@ -277,19 +276,12 @@ export class PersonService extends BaseService { await this.personRepository.vacuum({ reindexVectors: true }); } - let jobs: JobItem[] = []; - const assets = this.assetJobRepository.streamForDetectFacesJob(force); - for await (const asset of assets) { - jobs.push({ name: JobName.AssetDetectFaces, data: { id: asset.id } }); - - if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(jobs); - jobs = []; - } + for await (const assets of batched(this.assetJobRepository.streamForDetectFacesJob(force))) { + await this.jobRepository.queueAll( + assets.map((asset) => ({ name: JobName.AssetDetectFaces, data: { id: asset.id } })), + ); } - await this.jobRepository.queueAll(jobs); - if (force === undefined) { await this.jobRepository.queue({ name: JobName.PersonCleanup }); } @@ -435,22 +427,16 @@ export class PersonService extends BaseService { await this.databaseRepository.prewarm(VectorIndex.Face); const lastRun = new Date().toISOString(); - const facePagination = this.personRepository.getAllFaces( + + const faces = this.personRepository.getAllFaces( force ? undefined : { personId: null, sourceType: SourceType.MachineLearning }, ); - - let jobs: { name: JobName.FacialRecognition; data: { id: string; deferred: false } }[] = []; - for await (const face of facePagination) { - jobs.push({ name: JobName.FacialRecognition, data: { id: face.id, deferred: false } }); - - if (jobs.length === JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(jobs); - jobs = []; - } + for await (const batch of batched(faces)) { + await this.jobRepository.queueAll( + batch.map((face) => ({ name: JobName.FacialRecognition, data: { id: face.id, deferred: false } })), + ); } - await this.jobRepository.queueAll(jobs); - await this.systemMetadataRepository.set(SystemMetadataKey.FacialRecognitionState, { lastRun }); return JobStatus.Success; @@ -614,12 +600,8 @@ export class PersonService extends BaseService { return results; } - private async findOrFail(id: string) { - const person = await this.personRepository.getById(id); - if (!person) { - throw new BadRequestException('Person not found'); - } - return person; + private findOrFail(id: string) { + return findOrFail(() => this.personRepository.getById(id), 'Person'); } // TODO return a asset face response diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 41b1d5403c..31ca32f552 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -13,7 +13,7 @@ import { } from 'src/dtos/shared-link.dto'; import { Permission, SharedLinkType } from 'src/enum'; import { BaseService } from 'src/services/base.service'; -import { getExternalDomain, OpenGraphTags } from 'src/utils/misc'; +import { findOrFail, getExternalDomain, OpenGraphTags } from 'src/utils/misc'; @Injectable() export class SharedLinkService extends BaseService { @@ -143,12 +143,8 @@ export class SharedLinkService extends BaseService { } // TODO: replace `userId` with permissions and access control checks - private async findOrFail(userId: string, id: string) { - const sharedLink = await this.sharedLinkRepository.get(userId, id); - if (!sharedLink) { - throw new BadRequestException('Shared link not found'); - } - return sharedLink; + private findOrFail(userId: string, id: string) { + return findOrFail(() => this.sharedLinkRepository.get(userId, id), 'Shared link'); } async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 93b8944099..19a1774492 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; import { SystemConfig } from 'src/config'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; + import { OnEvent, OnJob } from 'src/decorators'; import { AssetVisibility, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; -import { JobItem, JobOf } from 'src/types'; -import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; +import { JobOf } from 'src/types'; +import { batched, getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; @Injectable() export class SmartInfoService extends BaseService { @@ -77,18 +77,10 @@ export class SmartInfoService extends BaseService { await this.databaseRepository.setDimensionSize(dimSize); } - let queue: JobItem[] = []; - const assets = this.assetJobRepository.streamForEncodeClip(force); - for await (const asset of assets) { - queue.push({ name: JobName.SmartSearch, data: { id: asset.id } }); - if (queue.length >= JOBS_ASSET_PAGINATION_SIZE) { - await this.jobRepository.queueAll(queue); - queue = []; - } + for await (const assets of batched(this.assetJobRepository.streamForEncodeClip(force))) { + await this.jobRepository.queueAll(assets.map((asset) => ({ name: JobName.SmartSearch, data: { id: asset.id } }))); } - await this.jobRepository.queueAll(queue); - return JobStatus.Success; } diff --git a/server/src/services/stack.service.spec.ts b/server/src/services/stack.service.spec.ts index d47d634f4f..933c7f39c5 100644 --- a/server/src/services/stack.service.spec.ts +++ b/server/src/services/stack.service.spec.ts @@ -85,8 +85,9 @@ describe(StackService.name, () => { it('should fail if stack could not be found', async () => { mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); + mocks.stack.getById.mockResolvedValue(void 0); - await expect(sut.get(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(Error); + await expect(sut.get(authStub.admin, 'stack-id')).rejects.toBeInstanceOf(BadRequestException); expect(mocks.access.stack.checkOwnerAccess).toHaveBeenCalled(); expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); @@ -124,8 +125,9 @@ describe(StackService.name, () => { it('should fail if stack could not be found', async () => { mocks.access.stack.checkOwnerAccess.mockResolvedValue(new Set(['stack-id'])); + mocks.stack.getById.mockResolvedValue(void 0); - await expect(sut.update(AuthFactory.create(), 'stack-id', {})).rejects.toBeInstanceOf(Error); + await expect(sut.update(AuthFactory.create(), 'stack-id', {})).rejects.toBeInstanceOf(BadRequestException); expect(mocks.stack.getById).toHaveBeenCalledWith('stack-id'); expect(mocks.stack.update).not.toHaveBeenCalled(); diff --git a/server/src/services/stack.service.ts b/server/src/services/stack.service.ts index 2dc9f1547e..9dc47a97bf 100644 --- a/server/src/services/stack.service.ts +++ b/server/src/services/stack.service.ts @@ -4,6 +4,7 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto'; import { Permission } from 'src/enum'; import { BaseService } from 'src/services/base.service'; +import { findOrFail } from 'src/utils/misc'; import { UUIDAssetIDParamDto } from 'src/validation'; @Injectable() @@ -77,12 +78,7 @@ export class StackService extends BaseService { await this.eventRepository.emit('StackUpdate', { stackId, userId: auth.user.id }); } - private async findOrFail(id: string) { - const stack = await this.stackRepository.getById(id); - if (!stack) { - throw new Error('Asset stack not found'); - } - - return stack; + private findOrFail(id: string) { + return findOrFail(() => this.stackRepository.getById(id), 'Asset stack'); } } diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index e4f2dcacf7..de731f46c6 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -402,8 +402,8 @@ export class StorageTemplateService extends BaseService { const substitutions: Record = { filename, ext: extension, - filetype: asset.type == AssetType.Image ? 'IMG' : 'VID', - filetypefull: asset.type == AssetType.Image ? 'IMAGE' : 'VIDEO', + filetype: asset.type === AssetType.Image ? 'IMG' : 'VID', + filetypefull: asset.type === AssetType.Image ? 'IMAGE' : 'VIDEO', assetId: asset.id, assetIdShort: asset.id.slice(-12), //just throw into the root if it doesn't belong to an album diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index 1c7195e4c3..3cd442b6a7 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -17,6 +17,7 @@ import { TagAssetTable } from 'src/schema/tables/tag-asset.table'; import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; import { updateLockedColumns } from 'src/utils/database'; +import { findOrFail } from 'src/utils/misc'; import { upsertTags } from 'src/utils/tag'; @Injectable() @@ -146,12 +147,8 @@ export class TagService extends BaseService { return JobStatus.Success; } - private async findOrFail(id: string) { - const tag = await this.tagRepository.get(id); - if (!tag) { - throw new BadRequestException('Tag not found'); - } - return tag; + private findOrFail(id: string) { + return findOrFail(() => this.tagRepository.get(id), 'Tag'); } private async updateTags(assetId: string) { diff --git a/server/src/services/trash.service.ts b/server/src/services/trash.service.ts index f1d368111e..368e16beb5 100644 --- a/server/src/services/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { OnEvent, OnJob } from 'src/decorators'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { TrashResponseDto } from 'src/dtos/trash.dto'; import { JobName, JobStatus, Permission, QueueName } from 'src/enum'; import { BaseService } from 'src/services/base.service'; +import { batched } from 'src/utils/misc'; @Injectable() export class TrashService extends BaseService { @@ -47,39 +47,16 @@ export class TrashService extends BaseService { @OnJob({ name: JobName.AssetEmptyTrash, queue: QueueName.BackgroundTask }) async handleEmptyTrash() { - const assets = this.trashRepository.getDeletedIds(); - let count = 0; - const batch: string[] = []; - for await (const { id } of assets) { - batch.push(id); - - if (batch.length === JOBS_ASSET_PAGINATION_SIZE) { - await this.handleBatch(batch); - count += batch.length; - batch.length = 0; - } + for await (const assets of batched(this.trashRepository.getDeletedIds())) { + await this.jobRepository.queueAll( + assets.map(({ id }) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: true } })), + ); + count += assets.length; } - await this.handleBatch(batch); - count += batch.length; - batch.length = 0; - this.logger.log(`Queued ${count} asset(s) for deletion from the trash`); return JobStatus.Success; } - - private async handleBatch(ids: string[]) { - this.logger.debug(`Queueing ${ids.length} asset(s) for deletion from the trash`); - await this.jobRepository.queueAll( - ids.map((assetId) => ({ - name: JobName.AssetDelete, - data: { - id: assetId, - deleteOnDisk: true, - }, - })), - ); - } } diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 15ea8c0598..f80a810616 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -17,6 +17,7 @@ import { JobName, UserMetadataKey, UserStatus } from 'src/enum'; import { UserFindOptions } from 'src/repositories/user.repository'; import { BaseService } from 'src/services/base.service'; import { getCalendarHeatmap } from 'src/services/shared/user-methods'; +import { findOrFail } from 'src/utils/misc'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; @Injectable() @@ -158,11 +159,7 @@ export class UserAdminService extends BaseService { return mapPreferences(newPreferences); } - private async findOrFail(id: string, options: UserFindOptions) { - const user = await this.userRepository.get(id, options); - if (!user) { - throw new BadRequestException('User not found'); - } - return user; + private findOrFail(id: string, options: UserFindOptions) { + return findOrFail(() => this.userRepository.get(id, options), 'User'); } } diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index c7a5bc4e02..1630fd9dd9 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -20,6 +20,7 @@ import { getCalendarHeatmap } from 'src/services/shared/user-methods'; import { JobOf, UserMetadataItem } from 'src/types'; import { ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; +import { findOrFail } from 'src/utils/misc'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; import { generateProfileImage } from 'src/utils/profile-image'; @@ -302,11 +303,7 @@ export class UserService extends BaseService { return DateTime.now().minus({ days: delayUntilDeletion }) > DateTime.fromJSDate(user.deletedAt); } - private async findOrFail(id: string, options: UserFindOptions) { - const user = await this.userRepository.get(id, options); - if (!user) { - throw new BadRequestException('User not found'); - } - return user; + private findOrFail(id: string, options: UserFindOptions) { + return findOrFail(() => this.userRepository.get(id, options), 'User'); } } diff --git a/server/src/services/workflow.service.ts b/server/src/services/workflow.service.ts index 850b4ac086..1d4ecb89a0 100644 --- a/server/src/services/workflow.service.ts +++ b/server/src/services/workflow.service.ts @@ -14,6 +14,7 @@ import { import { Permission } from 'src/enum'; import { PluginMethodSearchResponse } from 'src/repositories/plugin.repository'; import { BaseService } from 'src/services/base.service'; +import { findOrFail } from 'src/utils/misc'; import { getWorkflowTriggers, isMethodCompatible, resolveMethod } from 'src/utils/workflow'; @Injectable() @@ -104,11 +105,7 @@ export class WorkflowService extends BaseService { return results; } - private async findOrFail(id: string) { - const workflow = await this.workflowRepository.get(id); - if (!workflow) { - throw new BadRequestException('Workflow not found'); - } - return workflow; + private findOrFail(id: string) { + return findOrFail(() => this.workflowRepository.get(id), 'Workflow'); } } diff --git a/server/src/utils/bytes.ts b/server/src/utils/bytes.ts index 5e476f4dea..67e94c7aca 100644 --- a/server/src/utils/bytes.ts +++ b/server/src/utils/bytes.ts @@ -20,7 +20,7 @@ export function asHumanReadable(bytes: number, precision = 1): string { } } - return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`; + return `${remainder.toFixed(magnitude === 0 ? 0 : precision)} ${units[magnitude]}`; } // if an asset is jsonified in the DB before being returned, its buffer fields will be hex-encoded strings diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 0e7a9e6cdc..a514007987 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -1,4 +1,4 @@ -import { INestApplication } from '@nestjs/common'; +import { BadRequestException, INestApplication } from '@nestjs/common'; import { ApiBodyOptions, DocumentBuilder, @@ -14,7 +14,7 @@ import path from 'node:path'; import picomatch from 'picomatch'; import parse from 'picomatch/lib/parse'; import { SystemConfig } from 'src/config'; -import { CLIP_MODEL_INFO, endpointTags, serverVersion } from 'src/constants'; +import { CLIP_MODEL_INFO, JOBS_ASSET_PAGINATION_SIZE, endpointTags, serverVersion } from 'src/constants'; import { extraModels } from 'src/decorators'; import { ApiCustomExtension, ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -111,6 +111,32 @@ export const handlePromiseError = (promise: Promise, logger: LoggingReposi promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack)); }; +export const findOrFail = async (find: () => Promise, entity: string): Promise> => { + const value = await find(); + if (!value) { + throw new BadRequestException(`${entity} not found`); + } + + return value; +}; + +export async function* batched(items: AsyncIterable, size = JOBS_ASSET_PAGINATION_SIZE): AsyncGenerator { + let batch: T[] = []; + + for await (const item of items) { + batch.push(item); + + if (batch.length >= size) { + yield batch; + batch = []; + } + } + + if (batch.length > 0) { + yield batch; + } +} + export interface OpenGraphTags { title: string; description: string; diff --git a/server/src/utils/transform.ts b/server/src/utils/transform.ts index aa1fe8b7fc..76d5247de3 100644 --- a/server/src/utils/transform.ts +++ b/server/src/utils/transform.ts @@ -227,7 +227,7 @@ export const transformOcrBoundingBox = ( const { points: transformedPoints, currentWidth, currentHeight } = transformPoints(points, edits, imageDimensions); // Reorder points to maintain semantic ordering (topLeft, topRight, bottomRight, bottomLeft) - const netRotation = edits.find((e) => e.action == AssetEditAction.Rotate)?.parameters.angle ?? 0 % 360; + const netRotation = edits.find((e) => e.action === AssetEditAction.Rotate)?.parameters.angle ?? 0 % 360; const reorderedPoints = reorderQuadPointsForRotation(transformedPoints, netRotation); const [p1, p2, p3, p4] = reorderedPoints; diff --git a/server/test/medium/specs/services/album.service.spec.ts b/server/test/medium/specs/services/album.service.spec.ts new file mode 100644 index 0000000000..bc73007593 --- /dev/null +++ b/server/test/medium/specs/services/album.service.spec.ts @@ -0,0 +1,37 @@ +import { Kysely } from 'kysely'; +import { AlbumRepository } from 'src/repositories/album.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { UserRepository } from 'src/repositories/user.repository'; +import { DB } from 'src/schema'; +import { AlbumService } from 'src/services/album.service'; +import { newMediumService } from 'test/medium.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + return newMediumService(AlbumService, { + database: db || defaultDatabase, + real: [AlbumRepository, UserRepository], + mock: [LoggingRepository], + }); +}; + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(AlbumService.name, () => { + describe('database triggers', () => { + it('should cascade delete an album when the owner is deleted', async () => { + const { ctx } = setup(); + const { user } = await ctx.newUser(); + await ctx.newAlbum({ ownerId: user.id }); + + await ctx.get(UserRepository).delete({ id: user.id }, true); + + await expect(ctx.database.selectFrom('album').selectAll().execute()).resolves.toEqual([]); + await expect(ctx.database.selectFrom('album_user').selectAll().execute()).resolves.toEqual([]); + }); + }); +}); diff --git a/server/test/medium/specs/services/user.service.spec.ts b/server/test/medium/specs/services/user.service.spec.ts index 2eac06d674..85ab9a8c0d 100644 --- a/server/test/medium/specs/services/user.service.spec.ts +++ b/server/test/medium/specs/services/user.service.spec.ts @@ -1,6 +1,6 @@ import { Kysely } from 'kysely'; import { DateTime } from 'luxon'; -import { ImmichEnvironment, JobName, JobStatus } from 'src/enum'; +import { ImmichEnvironment, JobName, JobStatus, UserAvatarColor } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { EventRepository } from 'src/repositories/event.repository'; @@ -10,10 +10,17 @@ import { SystemMetadataRepository } from 'src/repositories/system-metadata.repos import { UserRepository } from 'src/repositories/user.repository'; import { DB } from 'src/schema'; import { UserService } from 'src/services/user.service'; +import { HumanReadableSize } from 'src/utils/bytes'; import { mediumFactory, newMediumService } from 'test/medium.factory'; import { factory } from 'test/small.factory'; import { getKyselyDB } from 'test/utils'; +const userLicense = { + licenseKey: 'IMCL-FF69-TUK1-RWZU-V9Q8-QGQS-S5GC-X4R2-UFK4', + activationKey: + 'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw', +}; + let defaultDatabase: Kysely; const setup = (db?: Kysely) => { @@ -38,9 +45,10 @@ describe(UserService.name, () => { const { sut, ctx } = setup(); ctx.getMock(EventRepository).emit.mockResolvedValue(); const user = mediumFactory.userInsert(); - await expect(sut.createUser({ name: user.name, email: user.email })).resolves.toEqual( - expect.objectContaining({ name: user.name, email: user.email }), - ); + const created = await sut.createUser({ name: user.name, email: user.email }); + expect(created).toEqual(expect.objectContaining({ name: user.name, email: user.email })); + + await expect(sut.get(created.id)).resolves.toMatchObject({ name: user.name, email: user.email }); }); it('should reject user with duplicate email', async () => { @@ -97,6 +105,38 @@ describe(UserService.name, () => { expect((result as any).password).toBeUndefined(); }); + + it('should not expose private fields', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + + await expect(sut.get(user.id)).resolves.not.toMatchObject({ + shouldChangePassword: expect.anything(), + storageLabel: expect.anything(), + }); + }); + }); + + describe('getMe', () => { + it('should get my user', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + + await expect(sut.getMe(auth)).resolves.toEqual( + expect.objectContaining({ id: user.id, email: user.email, quotaUsageInBytes: 0 }), + ); + }); + + it('should include license info', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + await sut.setLicense(auth, userLicense); + + await expect(sut.getMe(auth)).resolves.toMatchObject({ license: userLicense }); + }); }); describe('updateMe', () => { @@ -109,25 +149,169 @@ describe(UserService.name, () => { expect(before.updatedAt).toBeDefined(); expect(after.updatedAt).toBeDefined(); expect(before.updatedAt).not.toEqual(after.updatedAt); + + await expect(sut.getMe(auth)).resolves.toMatchObject({ + name: `${before.name} Updated`, + updatedAt: after.updatedAt, + }); + }); + + it('should update the name', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { name: 'Name' }; + + await expect(sut.updateMe(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMe(auth)).resolves.toMatchObject(dto); + }); + + it('should update the email', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { email: 'updated@immich.cloud' }; + + await expect(sut.updateMe(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMe(auth)).resolves.toMatchObject(dto); + }); + + it('should not allow an email that is already taken', async () => { + const { sut, ctx } = setup(); + const { user: user1 } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user2.id } }); + + await expect(sut.updateMe(auth, { email: user1.email })).rejects.toThrow('Email is not available'); + }); + + it('should update the avatar color', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { avatarColor: UserAvatarColor.Blue }; + + await expect(sut.updateMe(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMe(auth)).resolves.toMatchObject(dto); + }); + + it('should clear shouldChangePassword when the password is updated', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser({ shouldChangePassword: true }); + const auth = factory.auth({ user: { id: user.id } }); + + await expect(sut.updateMe(auth, { password: 'super-secret' })).resolves.toMatchObject({ + shouldChangePassword: false, + }); + await expect(sut.getMe(auth)).resolves.toMatchObject({ shouldChangePassword: false }); + }); + }); + + describe('updateMyPreferences', () => { + it('should update memories enabled', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { memories: { enabled: false } }; + + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject({ memories: { enabled: true } }); + await expect(sut.updateMyPreferences(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject(dto); + }); + + it('should update the download archive size', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { download: { archiveSize: 1_234_567 } }; + + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject({ + download: { archiveSize: 4 * HumanReadableSize.GiB }, + }); + await expect(sut.updateMyPreferences(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject(dto); + }); + + it('should update download include embedded videos', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { download: { includeEmbeddedVideos: true } }; + + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject({ + download: { includeEmbeddedVideos: false }, + }); + await expect(sut.updateMyPreferences(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject(dto); + }); + + it('should update the minimum face count', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + const dto = { people: { minimumFaces: 2 } }; + + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject({ people: { minimumFaces: 3 } }); + await expect(sut.updateMyPreferences(auth, dto)).resolves.toMatchObject(dto); + await expect(sut.getMyPreferences(auth)).resolves.toMatchObject(dto); }); }); describe('setLicense', () => { it('should set a license', async () => { - const license = { - licenseKey: 'IMCL-FF69-TUK1-RWZU-V9Q8-QGQS-S5GC-X4R2-UFK4', - activationKey: - 'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw', - }; const { sut, ctx } = setup(); const { user } = await ctx.newUser(); const auth = factory.auth({ user: { id: user.id } }); await expect(sut.getLicense(auth)).rejects.toThrowError(); - const after = await sut.setLicense(auth, license); - expect(after.licenseKey).toEqual(license.licenseKey); - expect(after.activationKey).toEqual(license.activationKey); + const after = await sut.setLicense(auth, userLicense); + expect(after.licenseKey).toEqual(userLicense.licenseKey); + expect(after.activationKey).toEqual(userLicense.activationKey); const response = await sut.getLicense(auth); expect(response).toEqual(after); + await expect(sut.getMe(auth)).resolves.toMatchObject({ license: after }); + }); + + it('should reject a license key that does not start with IMCL-', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + await expect( + sut.setLicense(auth, { + licenseKey: 'IMSV-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD', + activationKey: 'activationKey', + }), + ).rejects.toThrow('Invalid license key'); + }); + + it('should reject an invalid activation key', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + await expect( + sut.setLicense(auth, { ...userLicense, activationKey: `invalid${userLicense.activationKey}` }), + ).rejects.toThrow('Invalid license key'); + }); + }); + + describe('deleteLicense', () => { + it('should delete the license', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user: { id: user.id } }); + + await sut.setLicense(auth, userLicense); + await sut.deleteLicense(auth); + + await expect(sut.getLicense(auth)).rejects.toThrowError(); }); }); diff --git a/web/eslint.config.js b/web/eslint.config.js index de378d11c7..fc69385c93 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -10,15 +10,12 @@ import parser from 'svelte-eslint-parser'; import typescriptEslint from 'typescript-eslint'; import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); export default typescriptEslint.config( ...eslintPluginSvelte.configs.recommended, eslintPluginUnicorn.configs.recommended, js.configs.recommended, + prettier, { plugins: { tscompat: tslintPluginCompat, @@ -28,7 +25,7 @@ export default typescriptEslint.config( 'error', { browserslist: fs - .readFileSync(path.join(__dirname, '.browserslistrc'), 'utf8') + .readFileSync(path.join(import.meta.dirname, '.browserslistrc'), 'utf8') .split('\n') .map((line) => line.trim()) .filter((line) => line && !line.startsWith('#')), @@ -39,7 +36,7 @@ export default typescriptEslint.config( parser, parserOptions: { project: ['./tsconfig.json'], - tsconfigRootDir: __dirname, + tsconfigRootDir: import.meta.dirname, }, }, // ignores: ['**/service-worker/**'], @@ -95,7 +92,7 @@ export default typescriptEslint.config( parserOptions: { extraFileExtensions: ['.svelte'], - tsconfigRootDir: __dirname, + tsconfigRootDir: import.meta.dirname, project: ['./tsconfig.json'], }, }, @@ -158,6 +155,7 @@ export default typescriptEslint.config( 'svelte/button-has-type': 'error', 'object-shorthand': ['error', 'always'], 'svelte/no-navigation-without-resolve': 'off', + eqeqeq: 'error', }, }, { diff --git a/web/package.json b/web/package.json index 338e2f5abe..d709ca0de6 100644 --- a/web/package.json +++ b/web/package.json @@ -27,7 +27,7 @@ "@formatjs/icu-messageformat-parser": "^3.0.0", "@immich/justified-layout-wasm": "^0.4.3", "@immich/sdk": "workspace:*", - "@immich/ui": "^0.83.0", + "@immich/ui": "^0.85.0", "@mapbox/mapbox-gl-rtl-text": "0.4.0", "@mdi/js": "^7.4.47", "@noble/hashes": "^2.2.0", diff --git a/web/src/lib/actions/use-actions.ts b/web/src/lib/actions/use-actions.ts index 622615358d..0b09549a03 100644 --- a/web/src/lib/actions/use-actions.ts +++ b/web/src/lib/actions/use-actions.ts @@ -40,7 +40,7 @@ export function useActions(node: HTMLElement | SVGElement, actions: ActionArray) return { update(actions: ActionArray) { - if ((actions?.length || 0) != actionReturns.length) { + if ((actions?.length || 0) !== actionReturns.length) { throw new Error('You must not change the length of an actions array.'); } diff --git a/web/src/lib/components/asset-viewer/ActivityViewer.svelte b/web/src/lib/components/asset-viewer/ActivityViewer.svelte index 803495c8bb..01c970e627 100644 --- a/web/src/lib/components/asset-viewer/ActivityViewer.svelte +++ b/web/src/lib/components/asset-viewer/ActivityViewer.svelte @@ -99,7 +99,7 @@ }; $effect(() => { - if (assetId && previousAssetId != assetId) { + if (assetId && previousAssetId !== assetId) { previousAssetId = assetId; } }); @@ -168,7 +168,7 @@ {/if} - {#if (index != activityManager.activities.length - 1 && !shouldGroup(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1} + {#if (index !== activityManager.activities.length - 1 && !shouldGroup(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1}
{/if}
- {#if (index != activityManager.activities.length - 1 && isTenMinutesApart(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1} + {#if (index !== activityManager.activities.length - 1 && isTenMinutesApart(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1}
{/if} - {#if $slideshowState != SlideshowState.None} + {#if $slideshowState !== SlideshowState.None}
diff --git a/web/src/lib/components/asset-viewer/VideoRemoteViewer.svelte b/web/src/lib/components/asset-viewer/VideoRemoteViewer.svelte index 2ca123b879..040a22f04f 100644 --- a/web/src/lib/components/asset-viewer/VideoRemoteViewer.svelte +++ b/web/src/lib/components/asset-viewer/VideoRemoteViewer.svelte @@ -76,7 +76,7 @@ poster
- {#if castManager.castState == CastState.BUFFERING} + {#if castManager.castState === CastState.BUFFERING}
@@ -85,9 +85,9 @@ color="primary" shape="round" variant="ghost" - icon={castManager.castState == CastState.PLAYING ? mdiPause : mdiPlay} + icon={castManager.castState === CastState.PLAYING ? mdiPause : mdiPlay} onclick={() => handlePlayPauseButton()} - aria-label={castManager.castState == CastState.PLAYING ? 'Pause' : 'Play'} + aria-label={castManager.castState === CastState.PLAYING ? 'Pause' : 'Play'} /> {/if} diff --git a/web/src/lib/components/faces-page/PersonSidePanel.svelte b/web/src/lib/components/faces-page/PersonSidePanel.svelte index eaeabcc921..759bd03bce 100644 --- a/web/src/lib/components/faces-page/PersonSidePanel.svelte +++ b/web/src/lib/components/faces-page/PersonSidePanel.svelte @@ -357,7 +357,7 @@
{/if}
- {#if face.person != null} + {#if face.person !== null}
{ - if (lat != null && lng != null) { + if (lat !== undefined && lng !== undefined) { onUpdate(lat, lng); } }; diff --git a/web/src/lib/components/shared-components/map/Map.svelte b/web/src/lib/components/shared-components/map/Map.svelte index 52805d9b96..9811a8a7be 100644 --- a/web/src/lib/components/shared-components/map/Map.svelte +++ b/web/src/lib/components/shared-components/map/Map.svelte @@ -287,7 +287,7 @@ if (previousStyle) { // Preserves the custom map markers from the previous style when the theme is switched // Required until https://github.com/dimfeld/svelte-maplibre/issues/146 is fixed - const customLayers = previousStyle.layers.filter((l) => l.type == 'fill' && l.source == 'geojson'); + const customLayers = previousStyle.layers.filter((l) => l.type === 'fill' && l.source === 'geojson'); const layers = nextStyle.layers.concat(customLayers); const sources = nextStyle.sources; diff --git a/web/src/lib/elements/FormatMessage.svelte b/web/src/lib/elements/FormatMessage.svelte index 53f72451cc..cc097d783f 100644 --- a/web/src/lib/elements/FormatMessage.svelte +++ b/web/src/lib/elements/FormatMessage.svelte @@ -23,7 +23,7 @@ let { key, values = {}, children }: Props = $props(); const getLocale = (locale?: string | null) => { - if (locale == null) { + if (!locale) { throw new Error('Cannot format a message without first setting the initial locale.'); } diff --git a/web/src/lib/i18n.spec.ts b/web/src/lib/i18n.spec.ts index ebf2fdfa3b..bac5dba4b7 100644 --- a/web/src/lib/i18n.spec.ts +++ b/web/src/lib/i18n.spec.ts @@ -6,7 +6,7 @@ describe('i18n', () => { const languageFiles = readdirSync('../i18n').sort(); for (const filename of languageFiles) { test(`${filename} should have a loader`, async () => { - if (!filename.endsWith('.json') || filename == 'package.json') { + if (!filename.endsWith('.json') || filename === 'package.json') { return; } diff --git a/web/src/lib/managers/VirtualScrollManager/VirtualScrollManager.svelte.ts b/web/src/lib/managers/VirtualScrollManager/VirtualScrollManager.svelte.ts index fdbc86a7db..e3e72aec82 100644 --- a/web/src/lib/managers/VirtualScrollManager/VirtualScrollManager.svelte.ts +++ b/web/src/lib/managers/VirtualScrollManager/VirtualScrollManager.svelte.ts @@ -55,7 +55,7 @@ export abstract class VirtualScrollManager { } #setHeaderHeight(value: number) { - if (this.#headerHeight == value) { + if (this.#headerHeight === value) { return false; } this.#headerHeight = value; @@ -67,7 +67,7 @@ export abstract class VirtualScrollManager { } #setGap(value: number) { - if (this.#gap == value) { + if (this.#gap === value) { return false; } this.#gap = value; @@ -79,7 +79,7 @@ export abstract class VirtualScrollManager { } #setRowHeight(value: number) { - if (this.#rowHeight == value) { + if (this.#rowHeight === value) { return false; } this.#rowHeight = value; diff --git a/web/src/lib/modals/GeolocationUpdateConfirmModal.svelte b/web/src/lib/modals/GeolocationUpdateConfirmModal.svelte index 8230f62c73..a57dd1db6e 100644 --- a/web/src/lib/modals/GeolocationUpdateConfirmModal.svelte +++ b/web/src/lib/modals/GeolocationUpdateConfirmModal.svelte @@ -13,6 +13,7 @@ const { point, assetCount, onClose }: Props = $props(); const hasExistingLocations = $derived( + // eslint-disable-next-line eqeqeq assetMultiSelectManager.assets.some((asset) => asset.latitude != null || asset.longitude != null), ); diff --git a/web/src/lib/modals/timezone-utils.ts b/web/src/lib/modals/timezone-utils.ts index 2dfbb3fd5f..ae86e46321 100644 --- a/web/src/lib/modals/timezone-utils.ts +++ b/web/src/lib/modals/timezone-utils.ts @@ -95,7 +95,7 @@ function zoneOptionForDate(zone: string, date: string) { function sortTwoZones(zoneA: ZoneOption, zoneB: ZoneOption) { const offsetDifference = zoneA.offsetMinutes - zoneB.offsetMinutes; - if (offsetDifference != 0) { + if (offsetDifference !== 0) { return offsetDifference; } return zoneA.value.localeCompare(zoneB.value, undefined, { sensitivity: 'base' }); diff --git a/web/src/lib/stores/upload.ts b/web/src/lib/stores/upload.ts index 04a8a45bee..21929ab1e4 100644 --- a/web/src/lib/stores/upload.ts +++ b/web/src/lib/stores/upload.ts @@ -67,7 +67,7 @@ function createUploadStore() { const updateAssetMap = (id: string, mapper: (assets: UploadAsset) => UploadAsset) => { uploadAssets.update((uploadingAssets) => { return uploadingAssets.map((asset) => { - if (asset.id == id) { + if (asset.id === id) { return mapper(asset); } return asset; @@ -111,7 +111,7 @@ function createUploadStore() { }); } - return uploadingAsset.filter((a) => a.id != id); + return uploadingAsset.filter((a) => a.id !== id); }); }; diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts index 3624b34f93..f5733fd5b2 100644 --- a/web/src/lib/utils/actions.ts +++ b/web/src/lib/utils/actions.ts @@ -68,7 +68,7 @@ const undoDeleteAssets = async (onUndoDelete: OnUndoDelete, assets: TimelineAsse * @param {StackResponse} stackResponse - The stack response containing the stack and assets to delete. */ export function updateStackedAssetInTimeline(timelineManager: TimelineManager, { stack, toDeleteIds }: StackResponse) { - if (stack == undefined) { + if (stack === undefined) { return; } diff --git a/web/src/lib/utils/duplicate-utils.ts b/web/src/lib/utils/duplicate-utils.ts index 86b221906f..53a52ddcf9 100644 --- a/web/src/lib/utils/duplicate-utils.ts +++ b/web/src/lib/utils/duplicate-utils.ts @@ -147,6 +147,7 @@ const metadataFields = [ titleKey: 'gps', keys: ['latitude', 'longitude'], render: (asset, $t) => + // eslint-disable-next-line eqeqeq asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null ? `${asset.exifInfo.latitude.toFixed(4)}, ${asset.exifInfo.longitude.toFixed(4)}` : $t('unknown'), @@ -173,18 +174,21 @@ const metadataFields = [ icon: mdiCameraIris, titleKey: 'f_number', keys: ['fNumber'], + // eslint-disable-next-line eqeqeq render: (asset, $t) => (asset.exifInfo?.fNumber == null ? $t('unknown') : `f/${asset.exifInfo.fNumber.toFixed(1)}`), }, { icon: mdiRayStartArrow, titleKey: 'focal_length', keys: ['focalLength'], + // eslint-disable-next-line eqeqeq render: (asset, $t) => (asset.exifInfo?.focalLength == null ? $t('unknown') : `${asset.exifInfo.focalLength} mm`), }, { icon: mdiBrightness6, titleKey: 'iso', keys: ['iso'], + // eslint-disable-next-line eqeqeq render: (asset, $t) => (asset.exifInfo?.iso == null ? $t('unknown') : `ISO ${asset.exifInfo.iso}`), }, { @@ -203,6 +207,7 @@ const metadataFields = [ icon: mdiStarOutline, titleKey: 'rating', keys: ['rating'], + // eslint-disable-next-line eqeqeq render: (asset, $t) => (asset.exifInfo?.rating == null ? $t('unknown') : `${asset.exifInfo.rating} stars`), }, { diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts index 91513911f1..2f88da1f43 100644 --- a/web/src/lib/utils/navigation.ts +++ b/web/src/lib/utils/navigation.ts @@ -44,7 +44,7 @@ export function currentUrlReplaceAssetId(assetId: string) { // always remove the assetGridScrollTargetParams params.delete('at'); const paramsString = params.toString(); - const searchparams = paramsString == '' ? '' : '?' + params.toString(); + const searchparams = paramsString === '' ? '' : '?' + params.toString(); // this contains special casing for the /photos/:assetId photos route, which hangs directly // off / instead of a subpath, unlike every other asset-containing route. return isPhotosRoute(page.route.id) diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index 018d7d34ef..e1e717f45a 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -183,8 +183,8 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset): isFavorite: assetResponse.isFavorite, visibility: assetResponse.visibility, isTrashed: assetResponse.isTrashed, - isVideo: assetResponse.type == AssetTypeEnum.Video, - isImage: assetResponse.type == AssetTypeEnum.Image, + isVideo: assetResponse.type === AssetTypeEnum.Video, + isImage: assetResponse.type === AssetTypeEnum.Image, stack: assetResponse.stack || null, duration: assetResponse.duration || null, projectionType: assetResponse.exifInfo?.projectionType || null, diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 733c1f556a..c1028ad658 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -222,7 +222,7 @@ const onNameChangeSubmit = async (name: string, targetPerson: PersonResponseDto) => { try { - if (name == targetPerson.name) { + if (name === targetPerson.name) { return; } diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3fe707c6df..e4389f09dc 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -98,7 +98,7 @@ } else if ($page.params.assetId) { previousRoute = Route.viewPerson(data.person); } - if (action == 'merge') { + if (action === 'merge') { viewMode = PersonPageViewMode.MERGE_PEOPLE; } @@ -180,7 +180,7 @@ const [, personToBeMergedInto] = result; - if (personToBeMergedInto.name != personName && person.id === personToBeMergedInto.id) { + if (personToBeMergedInto.name !== personName && person.id === personToBeMergedInto.id) { await updateAssetCount(); return { merged: true }; } diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index e7a18f1d0f..6683dbfe2e 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -201,7 +201,7 @@ personIds.map(async (personId) => { const person = await getPerson({ id: personId }); - if (person.name == '') { + if (person.name === '') { return $t('no_name'); } diff --git a/web/src/routes/(user)/user-settings/AppSettings.svelte b/web/src/routes/(user)/user-settings/AppSettings.svelte index ddfac02bfc..04c8b6af06 100644 --- a/web/src/routes/(user)/user-settings/AppSettings.svelte +++ b/web/src/routes/(user)/user-settings/AppSettings.svelte @@ -74,7 +74,7 @@ - + {selectedDate} diff --git a/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte index f4c0a93655..338c463375 100644 --- a/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -38,7 +38,7 @@ }; const preAction = async (payload: Action) => { - if (payload.type == 'trash') { + if (payload.type === 'trash') { // eslint-disable-next-line @typescript-eslint/no-unused-expressions (await navigateToAsset(assetCursor?.nextAsset)) || (await navigateToAsset(assetCursor?.previousAsset)) || diff --git a/web/src/routes/admin/system-settings/JobSettings.svelte b/web/src/routes/admin/system-settings/JobSettings.svelte index a43be5e9f4..c229a788b4 100644 --- a/web/src/routes/admin/system-settings/JobSettings.svelte +++ b/web/src/routes/admin/system-settings/JobSettings.svelte @@ -66,7 +66,7 @@ description="" bind:value={configToEdit.job[queueName].concurrency} required={true} - isEdited={configToEdit.job[queueName].concurrency != config.job[queueName].concurrency} + isEdited={configToEdit.job[queueName].concurrency !== config.job[queueName].concurrency} /> {:else} { - if (nextStepIndex == -1) { + if (nextStepIndex === -1) { if (authManager.user.isAdmin) { await updateAdminOnboarding({ adminOnboardingUpdateDto: { isOnboarded: true } }); await serverConfigManager.loadServerConfig(); diff --git a/web/src/routes/auth/onboarding/OnboardingHello.svelte b/web/src/routes/auth/onboarding/OnboardingHello.svelte index 533de49f81..1b5b0d2e88 100644 --- a/web/src/routes/auth/onboarding/OnboardingHello.svelte +++ b/web/src/routes/auth/onboarding/OnboardingHello.svelte @@ -16,7 +16,7 @@ {$t('onboarding_welcome_user', { values: { user: authManager.user.name } })}

- {userRole == OnboardingRole.SERVER + {userRole === OnboardingRole.SERVER ? $t('onboarding_server_welcome_description') : $t('onboarding_user_welcome_description')}

diff --git a/web/vite.config.ts b/web/vite.config.ts index c2b2b5bf25..07e7efaf2a 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -28,8 +28,8 @@ export default defineConfig({ alias: { 'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js', // eslint-disable-next-line unicorn/prefer-module - '@test-data': path.resolve(__dirname, './src/test-data'), - // '@immich/ui': path.resolve(__dirname, '../../ui/packages/ui'), + '@test-data': path.resolve(import.meta.dirname, './src/test-data'), + // '@immich/ui': path.resolve(import.meta.dirname, '../../ui/packages/ui'), }, }, server: {